diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 3217f99f0..f759710e7 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -19,6 +19,12 @@ "commands": [ "fantomas" ] + }, + "fsharp-analyzers": { + "version": "0.23.0", + "commands": [ + "fsharp-analyzers" + ] } } } \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index 8bf0f719f..000000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,12 +0,0 @@ -### WHAT -copilot:summary - -copilot:poem - -copilot:emoji - -### WHY - - -### HOW -copilot:walkthrough diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9daa30c1d..ec3f4a257 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -84,16 +84,10 @@ jobs: run: dotnet --info - name: Restore tools - run: | - # can't do a tool restore here due to an 8.0.100 rc2 bug - this is fixed in GA so can go back to - # dotnet tool restore then. - dotnet tool install paket --version 8.0.0-alpha002 && dotnet tool install fantomas --version 6.2.3 && dotnet paket restore - - - name: cat file - run: cat src/FsAutoComplete.Core/AbstractClassStubGenerator.fs + run: dotnet tool restore - name: Check format - run: dotnet build -t:CheckFormat + run: dotnet fantomas --check src env: DOTNET_ROLL_FORWARD: LatestMajor DOTNET_ROLL_FORWARD_TO_PRERELEASE: 1 @@ -110,3 +104,23 @@ jobs: env: BuildNet7: ${{ matrix.build_net7 }} BuildNet8: ${{ matrix.build_net8 }} + + analyze: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + + - name: Restore tools + run: dotnet tool restore + + - name: Run analyzers + run: dotnet build -t:AnalyzeSolution -p:TargetFramework=net6.0 + + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: analysisreports diff --git a/.gitignore b/.gitignore index 48f5e612c..b34fbd3ac 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,5 @@ test/FsAutoComplete.Tests.Lsp/TestResults/ .tool-versions BenchmarkDotNet.Artifacts/ + +*.sarif \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a791b23a7..700b9d71b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,44 @@ # Changelog + +## [0.69.0] - 2024-01-14 + +### Added + +- [Codefix: Update value in signature file](https://github.com/fsharp/FsAutoComplete/pull/1161) and [1220](https://github.com/fsharp/FsAutoComplete/pull/1220) (thanks @nojaf) + +### Changed + +- [Analyzers: Update analyzers support to 0.23.0](https://github.com/fsharp/FsAutoComplete/pull/1217) (thanks @dawedawe) + +### Fixed + +- [fix and improves l10n](https://github.com/fsharp/FsAutoComplete/pull/1181) (thanks @Tangent-90) +- [Fix AP signatures for APs with names which are substrings of other APs](https://github.com/fsharp/FsAutoComplete/pull/1211) (thanks @dawedawe) +- [fixing caching of cancelled cached tasks](https://github.com/fsharp/FsAutoComplete/pull/1221) (thanks @TheAngryByrd) + +### Internal + + +## [0.68.0] - 2023-11-17 + +### Added + +* [Dotnet 8 support](https://github.com/fsharp/FsAutoComplete/pull/1175) (Thanks @baronfel & @TheAngryByrd) +* [F# 8 Support](https://github.com/fsharp/FsAutoComplete/pull/1180) (Thanks @baronfel & @nojaf & @dawedawe) + +### Changed + +* [Updates Ionide.LanguageServerProtocol to 0.4.20](https://github.com/fsharp/FsAutoComplete/pull/1190) (Thanks @TheAngryByrd) +* [Update IcedTasks 0.9.2](https://github.com/fsharp/FsAutoComplete/pull/1197) (Thanks @TheAngryByrd) +* [Paket Simplify](https://github.com/fsharp/FsAutoComplete/pull/1204) (Thanks @1eyewonder) + +### Fixed + +- [Do ordinal string comparisons](https://github.com/fsharp/FsAutoComplete/pull/1193) (Thanks @dawedawe) +- [fix typo in FullNameExternalAutocomplete default value](https://github.com/fsharp/FsAutoComplete/pull/1196) (Thanks @MrLuje) +* [Fix tooltip errorhandling]()(https://github.com/fsharp/FsAutoComplete/pull/1195) (Thanks @pblasucci & @TheAngryByrd) + ## [0.67.0] - 2023-10-28 ### Changed diff --git a/Directory.Build.props b/Directory.Build.props index 4b7f3db85..efd7cdc05 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,6 +6,8 @@ Apache-2.0 $(NoWarn);3186,0042 $(NoWarn);NU1902 + $(WarnOn);1182 + $(WarnOn);3390 true $(MSBuildThisFileDirectory)CHANGELOG.md diff --git a/Directory.Build.targets b/Directory.Build.targets index ac86c2874..013730228 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -8,5 +8,10 @@ + + ./ + . + --analyzers-path "$(PkgIonide_Analyzers)/analyzers/dotnet/fs" --code-root $(CodeRoot) --report "$(SarifOutput)/$(MSBuildProjectName)-$(TargetFramework).sarif" --verbosity d + diff --git a/Directory.Solution.targets b/Directory.Solution.targets index 649ad93a7..ece14a3d3 100644 --- a/Directory.Solution.targets +++ b/Directory.Solution.targets @@ -1,13 +1,25 @@ - - - - - + - + + + + + + + + + $(SolutionDir) + $(SolutionDir)/analysisreports + + + + diff --git a/benchmarks/Program.fs b/benchmarks/Program.fs index a4eb20197..faeecc5b3 100644 --- a/benchmarks/Program.fs +++ b/benchmarks/Program.fs @@ -1,4 +1,5 @@ namespace Benchmarks + open System open BenchmarkDotNet open BenchmarkDotNet.Attributes @@ -11,6 +12,6 @@ open System.Security.Cryptography module EntryPoint = [] - let main argv = - let summary = BenchmarkRunner.Run(); + let main _argv = + let _summary = BenchmarkRunner.Run() 0 diff --git a/benchmarks/paket.references b/benchmarks/paket.references index d29cc1cf0..50f1157c9 100644 --- a/benchmarks/paket.references +++ b/benchmarks/paket.references @@ -1,4 +1,3 @@ -FSharp.Core BenchmarkDotNet Microsoft.CodeAnalysis -FSharp.Data.Adaptive +FSharp.Data.Adaptive \ No newline at end of file diff --git a/build/Program.fs b/build/Program.fs index 55afc5000..3b5b6f060 100644 --- a/build/Program.fs +++ b/build/Program.fs @@ -43,8 +43,6 @@ let init args = Context.setExecutionContext (Context.RuntimeContext.Fake execContext) Target.initEnvironment () - let fsacAssemblies = "FsAutoComplete|FsAutoComplete.Core|LanguageServerProtocol" - let packAsToolProp = "PackAsTool", "true" Target.create "LspTest" (fun _ -> diff --git a/build/paket.references b/build/paket.references index aa0513d00..76fa44de9 100644 --- a/build/paket.references +++ b/build/paket.references @@ -1,18 +1,11 @@ group Build - Fake.Core.Target - Fake.Core.Process - Fake.DotNet.Cli - Fake.Core.ReleaseNotes - Fake.DotNet.AssemblyInfoFile - Fake.DotNet.Paket - Fake.Tools.Git - Fake.Core.Environment - Fake.Core.UserInput - Fake.IO.FileSystem - Fake.IO.Zip - Fake.DotNet.MsBuild - Fake.Api.GitHub - Microsoft.Build - MSBuild.StructuredLogger - Octokit - Fantomas.Core +Fake.Core.Target +Fake.Core.ReleaseNotes +Fake.DotNet.AssemblyInfoFile +Fake.DotNet.Paket +Fake.Tools.Git +Fake.Core.UserInput +Fake.IO.Zip +Fake.Api.GitHub +Microsoft.Build +Fantomas.Core \ No newline at end of file diff --git a/paket.dependencies b/paket.dependencies index 020d9c3e9..bfd5798a1 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -13,7 +13,9 @@ lowest_matching: true nuget BenchmarkDotNet 0.13.5 nuget Fantomas.Client >= 0.9 -nuget FSharp.Compiler.Service >= 43.8.200-preview.23562.1 +nuget FSharp.Compiler.Service >= 43.8.200-preview.24061.1 +nuget Ionide.Analyzers 0.7.0 +nuget FSharp.Analyzers.Build 0.3.0 nuget Ionide.ProjInfo >= 0.62.0 nuget Ionide.ProjInfo.FCS >= 0.62.0 nuget Ionide.ProjInfo.ProjectSystem >= 0.62.0 @@ -24,12 +26,10 @@ nuget Microsoft.Build.Utilities.Core >= 17.4 copy_local:false nuget Microsoft.Build.Tasks.Core >= 17.4 copy_local: false nuget Nuget.Frameworks >= 6.3 copy_local: false nuget Microsoft.CodeAnalysis 4.5.0 -nuget FSharp.Analyzers.SDK +nuget FSharp.Analyzers.SDK 0.23.0 nuget ICSharpCode.Decompiler nuget Mono.Cecil >= 0.11.4 -nuget Newtonsoft.Json nuget FSharpLint.Core -nuget System.Configuration.ConfigurationManager nuget Serilog >= 2.10.0 nuget Serilog.Sinks.File >= 5.0.0 nuget Serilog.Sinks.Console >= 4.0.0 @@ -38,7 +38,7 @@ nuget Destructurama.FSharp nuget FSharp.UMX >= 1.1 nuget FSharp.Formatting >= 14.0 nuget FsToolkit.ErrorHandling.TaskResult >= 4.4 framework: netstandard2.1 ,net6.0, net7.0, net8.0 -nuget IcedTasks >= 0.5 +nuget IcedTasks >= 0.9.2 nuget FSharpx.Async >= 1.14 nuget CliWrap >= 3.0 nuget System.CommandLine prerelease @@ -48,18 +48,19 @@ nuget Dotnet.ReproducibleBuilds copy_local:true nuget Microsoft.NETFramework.ReferenceAssemblies nuget Ionide.KeepAChangelog.Tasks copy_local: true -nuget Expecto nuget Expecto.Diff nuget YoloDev.Expecto.TestSdk nuget AltCover nuget GitHubActionsTestLogger -nuget Ionide.LanguageServerProtocol >= 0.4.19 +nuget Ionide.LanguageServerProtocol >= 0.4.20 nuget Microsoft.Extensions.Caching.Memory nuget OpenTelemetry.Api >= 1.3.2 nuget OpenTelemetry.Exporter.OpenTelemetryProtocol >= 1.3.2 # 1.4 bumps to 7.0 versions of System.Diagnostics libs, so can't use it nuget LinkDotNet.StringBuilder 1.18.0 nuget CommunityToolkit.HighPerformance nuget System.Security.Cryptography.Pkcs 6.0.4 +nuget System.Net.Http 4.3.4 # pinned for security reasons +nuget System.Text.RegularExpressions 4.3.1 # pinned for security reasons group Build @@ -69,24 +70,17 @@ group Build framework: net6.0, net7.0 nuget Fake.Core.Target - nuget Fake.Core.Process - nuget Fake.DotNet.Cli nuget Fake.Core.ReleaseNotes nuget Fake.DotNet.AssemblyInfoFile nuget Fake.DotNet.Paket nuget Fake.Tools.Git - nuget Fake.Core.Environment nuget Fake.Core.UserInput - nuget Fake.IO.FileSystem nuget Fake.IO.Zip - nuget Fake.DotNet.MsBuild nuget Fake.Api.GitHub nuget Microsoft.Build - nuget MSBuild.StructuredLogger nuget Octokit 0.48 // there's an API break in 0.50, so we need to pin this to prevent errors nuget Fantomas.Core 6.2.0 nuget NuGet.Versioning 6.7.0 nuget NuGet.Common 6.7.0 nuget NuGet.Protocol 6.7.0 - diff --git a/paket.lock b/paket.lock index 43bc908fe..d83ca04b7 100644 --- a/paket.lock +++ b/paket.lock @@ -55,10 +55,12 @@ NUGET StreamJsonRpc (>= 2.8.28) FParsec (1.1.1) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net5.0)) (&& (== netstandard2.1) (>= net5.0)) FSharp.Core (>= 4.3.4) - FSharp.Analyzers.SDK (0.11) - FSharp.Compiler.Service (>= 41.0.1) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net5.0)) (&& (== netstandard2.1) (>= net5.0)) - FSharp.Core (>= 6.0.1) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net5.0)) (&& (== netstandard2.1) (>= net5.0)) - McMaster.NETCore.Plugins (>= 1.4) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net5.0)) (&& (== netstandard2.1) (>= net5.0)) + FSharp.Analyzers.Build (0.3) + FSharp.Analyzers.SDK (0.23) + FSharp.Compiler.Service (43.8.100) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) + FSharp.Core (8.0.100) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) + McMaster.NETCore.Plugins (>= 1.4) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) + Microsoft.Extensions.Logging.Abstractions (>= 6.0) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) FSharp.Control.AsyncSeq (3.2.1) FSharp.Core (>= 4.7.2) Microsoft.Bcl.AsyncInterfaces (>= 5.0) @@ -113,17 +115,18 @@ NUGET Grpc.Core.Api (>= 2.51) Humanizer.Core (2.14.1) Iced (1.17) - IcedTasks (0.5.4) - FSharp.Core (>= 7.0) + IcedTasks (0.9.2) + FSharp.Core (>= 6.0.1) ICSharpCode.Decompiler (7.2.1.6856) Microsoft.Win32.Registry (>= 5.0) System.Collections.Immutable (>= 5.0) System.Reflection.Metadata (>= 5.0) + Ionide.Analyzers (0.7) Ionide.KeepAChangelog.Tasks (0.1.8) - copy_local: true - Ionide.LanguageServerProtocol (0.4.19) + Ionide.LanguageServerProtocol (0.4.20) FSharp.Core (>= 6.0) Newtonsoft.Json (>= 13.0.1) - StreamJsonRpc (>= 2.10.44) + StreamJsonRpc (>= 2.16.36) Ionide.ProjInfo (0.62) FSharp.Core (>= 7.0.400) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) Ionide.ProjInfo.Sln (>= 0.62) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) @@ -144,20 +147,21 @@ NUGET Newtonsoft.Json (>= 13.0.1) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) Ionide.ProjInfo.Sln (0.62) LinkDotNet.StringBuilder (1.18) - McMaster.NETCore.Plugins (1.4) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net5.0)) (&& (== netstandard2.1) (>= net5.0)) + McMaster.NETCore.Plugins (1.4) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) Microsoft.DotNet.PlatformAbstractions (>= 3.1.6) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) (&& (== netstandard2.1) (>= netcoreapp2.1)) Microsoft.Extensions.DependencyModel (>= 5.0) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) (&& (== netstandard2.1) (>= netcoreapp2.1)) - MessagePack (2.4.35) - MessagePack.Annotations (>= 2.4.35) - Microsoft.Bcl.AsyncInterfaces (>= 6.0) - restriction: || (&& (== net6.0) (< netcoreapp3.1)) (&& (== net7.0) (< netcoreapp3.1)) (&& (== net8.0) (< netcoreapp3.1)) (== netstandard2.0) (== netstandard2.1) - Microsoft.NET.StringTools (>= 1.0) - System.Collections.Immutable (>= 1.7.1) - restriction: || (&& (== net6.0) (< netcoreapp3.1)) (&& (== net7.0) (< netcoreapp3.1)) (&& (== net8.0) (< netcoreapp3.1)) (== netstandard2.0) (== netstandard2.1) - System.Reflection.Emit (>= 4.7) - restriction: || (&& (== net6.0) (< netcoreapp3.1)) (&& (== net7.0) (< netcoreapp3.1)) (&& (== net8.0) (< netcoreapp3.1)) (== netstandard2.0) (== netstandard2.1) - System.Reflection.Emit.Lightweight (>= 4.7) - restriction: || (&& (== net6.0) (< netcoreapp3.1)) (&& (== net7.0) (< netcoreapp3.1)) (&& (== net8.0) (< netcoreapp3.1)) (== netstandard2.0) (== netstandard2.1) - System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: || (&& (== net6.0) (< netcoreapp3.1)) (&& (== net7.0) (< netcoreapp3.1)) (&& (== net8.0) (< netcoreapp3.1)) (== netstandard2.0) (== netstandard2.1) - MessagePack.Annotations (2.4.35) - Microsoft.Bcl.AsyncInterfaces (6.0) - System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netstandard2.1)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< netstandard2.1)) (&& (== net8.0) (>= net461)) (&& (== net8.0) (< netstandard2.1)) (== netstandard2.0) (&& (== netstandard2.1) (>= net461)) + MessagePack (2.5.108) + MessagePack.Annotations (>= 2.5.108) + Microsoft.Bcl.AsyncInterfaces (>= 6.0) - restriction: || (&& (== net7.0) (< net6.0)) (&& (== net8.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) + Microsoft.NET.StringTools (>= 17.4) + System.Collections.Immutable (>= 6.0) - restriction: || (&& (== net7.0) (< net6.0)) (&& (== net8.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) + System.Reflection.Emit (>= 4.7) - restriction: || (&& (== net7.0) (< net6.0)) (&& (== net8.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) + System.Reflection.Emit.Lightweight (>= 4.7) - restriction: || (&& (== net7.0) (< net6.0)) (&& (== net8.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) + System.Runtime.CompilerServices.Unsafe (>= 6.0) + System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: || (&& (== net7.0) (< net6.0)) (&& (== net8.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) + MessagePack.Annotations (2.5.108) + Microsoft.Bcl.AsyncInterfaces (7.0) + System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net462)) (&& (== net6.0) (< netstandard2.1)) (&& (== net7.0) (>= net462)) (&& (== net7.0) (< netstandard2.1)) (&& (== net8.0) (>= net462)) (&& (== net8.0) (< netstandard2.1)) (== netstandard2.0) (&& (== netstandard2.1) (>= net462)) Microsoft.Bcl.HashCode (1.1) - restriction: || (&& (== net6.0) (< netcoreapp2.1) (< netstandard2.1)) (&& (== net7.0) (< netcoreapp2.1) (< netstandard2.1)) (&& (== net8.0) (< netcoreapp2.1) (< netstandard2.1)) (== netstandard2.0) Microsoft.Build (17.2) - copy_local: false Microsoft.Build.Framework (>= 17.2) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net472)) (&& (== netstandard2.1) (>= net6.0)) @@ -263,7 +267,7 @@ NUGET Microsoft.Extensions.DependencyInjection.Abstractions (6.0) Microsoft.Bcl.AsyncInterfaces (>= 6.0) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netstandard2.1)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< netstandard2.1)) (&& (== net8.0) (>= net461)) (&& (== net8.0) (< netstandard2.1)) (== netstandard2.0) (&& (== netstandard2.1) (>= net461)) System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netstandard2.1)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< netstandard2.1)) (&& (== net8.0) (>= net461)) (&& (== net8.0) (< netstandard2.1)) (== netstandard2.0) (&& (== netstandard2.1) (>= net461)) - Microsoft.Extensions.DependencyModel (6.0) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net5.0)) (&& (== netstandard2.1) (>= net5.0)) + Microsoft.Extensions.DependencyModel (6.0) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) System.Buffers (>= 4.5.1) System.Memory (>= 4.5.4) System.Runtime.CompilerServices.Unsafe (>= 6.0) @@ -277,8 +281,6 @@ NUGET Microsoft.Extensions.Options (>= 6.0) System.Diagnostics.DiagnosticSource (>= 6.0) Microsoft.Extensions.Logging.Abstractions (6.0.2) - System.Buffers (>= 4.5.1) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< net6.0)) (&& (== net8.0) (>= net461)) (&& (== net8.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) - System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< net6.0)) (&& (== net8.0) (>= net461)) (&& (== net8.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) Microsoft.Extensions.Logging.Configuration (6.0) Microsoft.Extensions.Configuration (>= 6.0) Microsoft.Extensions.Configuration.Abstractions (>= 6.0) @@ -307,6 +309,8 @@ NUGET Microsoft.NET.Test.Sdk (17.4.1) Microsoft.CodeCoverage (>= 17.4.1) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net462)) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= net462)) (&& (== netstandard2.1) (>= netcoreapp3.1)) Microsoft.TestPlatform.TestHost (>= 17.4.1) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= netcoreapp3.1)) + Microsoft.NETCore.Platforms (1.1.1) + Microsoft.NETCore.Targets (1.1.3) Microsoft.NETFramework.ReferenceAssemblies (1.0.3) Microsoft.SourceLink.AzureRepos.Git (1.1.1) - copy_local: true Microsoft.Build.Tasks.Git (>= 1.1.1) @@ -327,14 +331,14 @@ NUGET Microsoft.TestPlatform.TestHost (17.4.1) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= netcoreapp3.1)) Microsoft.TestPlatform.ObjectModel (>= 17.4.1) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= netcoreapp3.1)) Newtonsoft.Json (>= 13.0.1) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= netcoreapp3.1)) - Microsoft.VisualStudio.Threading (17.3.44) - Microsoft.Bcl.AsyncInterfaces (>= 6.0) - Microsoft.VisualStudio.Threading.Analyzers (>= 17.3.44) - Microsoft.VisualStudio.Validation (>= 17.0.58) + Microsoft.VisualStudio.Threading (17.6.40) + Microsoft.Bcl.AsyncInterfaces (>= 7.0) + Microsoft.VisualStudio.Threading.Analyzers (>= 17.6.40) + Microsoft.VisualStudio.Validation (>= 17.0.71) Microsoft.Win32.Registry (>= 5.0) System.Threading.Tasks.Extensions (>= 4.5.4) - Microsoft.VisualStudio.Threading.Analyzers (17.3.44) - Microsoft.VisualStudio.Validation (17.0.64) + Microsoft.VisualStudio.Threading.Analyzers (17.6.40) + Microsoft.VisualStudio.Validation (17.6.11) 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)) (&& (== net7.0) (>= monoandroid) (< netstandard1.3)) (&& (== net7.0) (>= monotouch)) (&& (== net7.0) (< netcoreapp2.0)) (&& (== net7.0) (>= xamarinios)) (&& (== net7.0) (>= xamarinmac)) (&& (== net7.0) (>= xamarintvos)) (&& (== net7.0) (>= xamarinwatchos)) (&& (== net8.0) (>= monoandroid) (< netstandard1.3)) (&& (== net8.0) (>= monotouch)) (&& (== net8.0) (< netcoreapp2.0)) (&& (== net8.0) (>= xamarinios)) (&& (== net8.0) (>= xamarinmac)) (&& (== net8.0) (>= xamarintvos)) (&& (== net8.0) (>= xamarinwatchos)) (== netstandard2.0) (== netstandard2.1) System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (< netcoreapp2.0)) (&& (== net6.0) (< netcoreapp2.1)) (&& (== net6.0) (>= uap10.1)) (&& (== net7.0) (< netcoreapp2.0)) (&& (== net7.0) (< netcoreapp2.1)) (&& (== net7.0) (>= uap10.1)) (&& (== net8.0) (< netcoreapp2.0)) (&& (== net8.0) (< netcoreapp2.1)) (&& (== net8.0) (>= uap10.1)) (== netstandard2.0) (== netstandard2.1) @@ -342,12 +346,12 @@ NUGET System.Security.Principal.Windows (>= 5.0) Microsoft.Win32.SystemEvents (7.0) - copy_local: false, restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) Mono.Cecil (0.11.4) - Nerdbank.Streams (2.8.61) - Microsoft.Bcl.AsyncInterfaces (>= 5.0) - Microsoft.VisualStudio.Threading (>= 16.10.56) - Microsoft.VisualStudio.Validation (>= 16.10.26) - System.IO.Pipelines (>= 5.0.1) - System.Runtime.CompilerServices.Unsafe (>= 5.0) + Nerdbank.Streams (2.10.66) + Microsoft.Bcl.AsyncInterfaces (>= 7.0) + Microsoft.VisualStudio.Threading (>= 17.6.40) + Microsoft.VisualStudio.Validation (>= 17.6.11) + System.IO.Pipelines (>= 7.0) + System.Runtime.CompilerServices.Unsafe (>= 6.0) Newtonsoft.Json (13.0.2) NuGet.Frameworks (6.3) - copy_local: false OpenTelemetry (1.3.2) @@ -366,6 +370,36 @@ NUGET OpenTelemetry (>= 1.3.2) Perfolizer (0.2.1) System.Memory (>= 4.5.3) + runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.2) + runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.2) + runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.2) + runtime.native.System (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + runtime.native.System.Net.Http (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + runtime.native.System.Security.Cryptography.Apple (4.3) + runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple (>= 4.3) + runtime.native.System.Security.Cryptography.OpenSsl (4.3.2) + runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.2) + runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.2) + runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.2) + runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.2) + runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.2) + runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.2) + runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.2) + runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.2) + runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.2) + runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.2) + runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.2) + runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.2) + runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple (4.3) + runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.2) + runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.2) + runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.2) + runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.2) + runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.2) SemanticVersioning (2.0.2) Serilog (2.11) Serilog.Sinks.Async (1.5) @@ -374,18 +408,37 @@ NUGET Serilog (>= 2.10) Serilog.Sinks.File (5.0) Serilog (>= 2.10) - StreamJsonRpc (2.12.27) - MessagePack (>= 2.3.85) - Microsoft.Bcl.AsyncInterfaces (>= 6.0) - Microsoft.VisualStudio.Threading (>= 17.1.46) - Nerdbank.Streams (>= 2.8.57) + StreamJsonRpc (2.16.36) + MessagePack (>= 2.5.108) + Microsoft.Bcl.AsyncInterfaces (>= 7.0) + Microsoft.VisualStudio.Threading (>= 17.6.40) + Microsoft.VisualStudio.Threading.Analyzers (>= 17.6.40) + Microsoft.VisualStudio.Validation (>= 17.6.11) + Nerdbank.Streams (>= 2.10.66) Newtonsoft.Json (>= 13.0.1) - System.Collections.Immutable (>= 6.0) - System.Diagnostics.DiagnosticSource (>= 6.0) - System.IO.Pipelines (>= 6.0.3) - System.Threading.Tasks.Dataflow (>= 6.0) + System.Collections.Immutable (>= 7.0) + System.Diagnostics.DiagnosticSource (>= 7.0.2) + System.IO.Pipelines (>= 7.0) + System.Text.Encodings.Web (>= 7.0) + System.Text.Json (>= 7.0.3) + System.Threading.Tasks.Dataflow (>= 7.0) System.Buffers (4.5.1) System.CodeDom (6.0) - copy_local: false + System.Collections (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Collections.Concurrent (4.3) + System.Collections (>= 4.3) + System.Diagnostics.Debug (>= 4.3) + System.Diagnostics.Tracing (>= 4.3) + System.Globalization (>= 4.3) + System.Reflection (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Threading (>= 4.3) + System.Threading.Tasks (>= 4.3) System.Collections.Immutable (7.0) System.Memory (>= 4.5.5) - restriction: || (&& (== net6.0) (>= net462)) (&& (== net7.0) (>= net462)) (&& (== net7.0) (< net6.0)) (&& (== net8.0) (>= net462)) (&& (== net8.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) System.Runtime.CompilerServices.Unsafe (>= 6.0) - restriction: || (== net6.0) (&& (== net7.0) (>= net462)) (&& (== net7.0) (< net6.0)) (&& (== net8.0) (>= net462)) (&& (== net8.0) (< net6.0)) (&& (== net8.0) (< net7.0)) (== netstandard2.0) (== netstandard2.1) @@ -408,29 +461,114 @@ NUGET System.Composition.AttributedModel (>= 6.0) System.Composition.Hosting (>= 6.0) System.Composition.Runtime (>= 6.0) - System.Configuration.ConfigurationManager (6.0) + System.Configuration.ConfigurationManager (6.0) - copy_local: false System.Security.Cryptography.ProtectedData (>= 6.0) System.Security.Permissions (>= 6.0) + System.Diagnostics.Debug (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) System.Diagnostics.DiagnosticSource (7.0.2) System.Memory (>= 4.5.5) - restriction: || (&& (== net6.0) (>= net462)) (&& (== net7.0) (>= net462)) (&& (== net7.0) (< net6.0)) (&& (== net8.0) (>= net462)) (&& (== net8.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) System.Runtime.CompilerServices.Unsafe (>= 6.0) - restriction: || (== net6.0) (&& (== net7.0) (>= net462)) (&& (== net7.0) (< net6.0)) (&& (== net8.0) (>= net462)) (&& (== net8.0) (< net6.0)) (&& (== net8.0) (< net7.0)) (== netstandard2.0) (== netstandard2.1) + System.Diagnostics.Tracing (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) System.Drawing.Common (7.0) - copy_local: false, restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) Microsoft.Win32.SystemEvents (>= 7.0) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) System.Formats.Asn1 (6.0) System.Buffers (>= 4.5.1) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< net6.0)) (&& (== net8.0) (>= net461)) (&& (== net8.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< net6.0)) (&& (== net8.0) (>= net461)) (&& (== net8.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) - System.IO.Pipelines (6.0.3) - System.Buffers (>= 4.5.1) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netcoreapp3.1)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< netcoreapp3.1)) (&& (== net8.0) (>= net461)) (&& (== net8.0) (< netcoreapp3.1)) (== netstandard2.0) (== netstandard2.1) - System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netcoreapp3.1)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< netcoreapp3.1)) (&& (== net8.0) (>= net461)) (&& (== net8.0) (< netcoreapp3.1)) (== netstandard2.0) (== netstandard2.1) - System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netcoreapp3.1)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< netcoreapp3.1)) (&& (== net8.0) (>= net461)) (&& (== net8.0) (< netcoreapp3.1)) (== netstandard2.0) (== netstandard2.1) + System.Globalization (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Globalization.Calendars (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Globalization (>= 4.3) + System.Runtime (>= 4.3) + System.Globalization.Extensions (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + System.Globalization (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Runtime.InteropServices (>= 4.3) + System.IO (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Threading.Tasks (>= 4.3) + System.IO.FileSystem (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.IO (>= 4.3) + System.IO.FileSystem.Primitives (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Handles (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Threading.Tasks (>= 4.3) + System.IO.FileSystem.Primitives (4.3) + System.Runtime (>= 4.3) + System.IO.Pipelines (7.0) + System.Buffers (>= 4.5.1) - restriction: || (&& (== net6.0) (>= net462)) (&& (== net7.0) (>= net462)) (&& (== net7.0) (< net6.0)) (&& (== net8.0) (>= net462)) (&& (== net8.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) + System.Memory (>= 4.5.5) - restriction: || (&& (== net6.0) (>= net462)) (&& (== net7.0) (>= net462)) (&& (== net7.0) (< net6.0)) (&& (== net8.0) (>= net462)) (&& (== net8.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) + System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net462)) (&& (== net7.0) (>= net462)) (&& (== net7.0) (< net6.0)) (&& (== net8.0) (>= net462)) (&& (== net8.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) + System.Linq (4.3) + System.Collections (>= 4.3) + System.Diagnostics.Debug (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) System.Management (6.0) System.CodeDom (>= 6.0) System.Memory (4.5.5) System.Buffers (>= 4.5.1) - restriction: || (&& (== net6.0) (>= monotouch)) (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netcoreapp2.0)) (&& (== net6.0) (< netstandard1.1)) (&& (== net6.0) (< netstandard2.0)) (&& (== net6.0) (>= xamarinios)) (&& (== net6.0) (>= xamarinmac)) (&& (== net6.0) (>= xamarintvos)) (&& (== net6.0) (>= xamarinwatchos)) (&& (== net7.0) (>= monotouch)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< netcoreapp2.0)) (&& (== net7.0) (< netstandard1.1)) (&& (== net7.0) (< netstandard2.0)) (&& (== net7.0) (>= xamarinios)) (&& (== net7.0) (>= xamarinmac)) (&& (== net7.0) (>= xamarintvos)) (&& (== net7.0) (>= xamarinwatchos)) (&& (== net8.0) (>= monotouch)) (&& (== net8.0) (>= net461)) (&& (== net8.0) (< netcoreapp2.0)) (&& (== net8.0) (< netstandard1.1)) (&& (== net8.0) (< netstandard2.0)) (&& (== net8.0) (>= xamarinios)) (&& (== net8.0) (>= xamarinmac)) (&& (== net8.0) (>= xamarintvos)) (&& (== net8.0) (>= xamarinwatchos)) (== netstandard2.0) (== netstandard2.1) System.Numerics.Vectors (>= 4.4) - restriction: || (&& (== net6.0) (< netcoreapp2.0)) (&& (== net7.0) (< netcoreapp2.0)) (&& (== net8.0) (< netcoreapp2.0)) (== netstandard2.0) (== netstandard2.1) System.Runtime.CompilerServices.Unsafe (>= 4.5.3) - restriction: || (&& (== net6.0) (>= monotouch)) (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netcoreapp2.0)) (&& (== net6.0) (< netcoreapp2.1)) (&& (== net6.0) (< netstandard1.1)) (&& (== net6.0) (< netstandard2.0)) (&& (== net6.0) (>= uap10.1)) (&& (== net6.0) (>= xamarinios)) (&& (== net6.0) (>= xamarinmac)) (&& (== net6.0) (>= xamarintvos)) (&& (== net6.0) (>= xamarinwatchos)) (&& (== net7.0) (>= monotouch)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< netcoreapp2.0)) (&& (== net7.0) (< netcoreapp2.1)) (&& (== net7.0) (< netstandard1.1)) (&& (== net7.0) (< netstandard2.0)) (&& (== net7.0) (>= uap10.1)) (&& (== net7.0) (>= xamarinios)) (&& (== net7.0) (>= xamarinmac)) (&& (== net7.0) (>= xamarintvos)) (&& (== net7.0) (>= xamarinwatchos)) (&& (== net8.0) (>= monotouch)) (&& (== net8.0) (>= net461)) (&& (== net8.0) (< netcoreapp2.0)) (&& (== net8.0) (< netcoreapp2.1)) (&& (== net8.0) (< netstandard1.1)) (&& (== net8.0) (< netstandard2.0)) (&& (== net8.0) (>= uap10.1)) (&& (== net8.0) (>= xamarinios)) (&& (== net8.0) (>= xamarinmac)) (&& (== net8.0) (>= xamarintvos)) (&& (== net8.0) (>= xamarinwatchos)) (== netstandard2.0) (== netstandard2.1) + System.Net.Http (4.3.4) + Microsoft.NETCore.Platforms (>= 1.1.1) + runtime.native.System (>= 4.3) + runtime.native.System.Net.Http (>= 4.3) + runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.2) + System.Collections (>= 4.3) + System.Diagnostics.Debug (>= 4.3) + System.Diagnostics.DiagnosticSource (>= 4.3) + System.Diagnostics.Tracing (>= 4.3) + System.Globalization (>= 4.3) + System.Globalization.Extensions (>= 4.3) + System.IO (>= 4.3) + System.IO.FileSystem (>= 4.3) + System.Net.Primitives (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Runtime.Handles (>= 4.3) + System.Runtime.InteropServices (>= 4.3) + System.Security.Cryptography.Algorithms (>= 4.3) + System.Security.Cryptography.Encoding (>= 4.3) + System.Security.Cryptography.OpenSsl (>= 4.3) + System.Security.Cryptography.Primitives (>= 4.3) + System.Security.Cryptography.X509Certificates (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Threading (>= 4.3) + System.Threading.Tasks (>= 4.3) + System.Net.Primitives (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Runtime.Handles (>= 4.3) System.Numerics.Vectors (4.5) - restriction: || (&& (== net7.0) (< net6.0)) (&& (== net8.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) System.Reactive (5.0) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) + System.Reflection (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.IO (>= 4.3) + System.Reflection.Primitives (>= 4.3) + System.Runtime (>= 4.3) System.Reflection.Emit (4.7) System.Reflection.Emit.ILGeneration (>= 4.7) - restriction: || (&& (== net6.0) (< netcoreapp2.0) (< netstandard2.1)) (&& (== net6.0) (< netstandard1.1)) (&& (== net6.0) (< netstandard2.0)) (&& (== net6.0) (>= uap10.1)) (&& (== net7.0) (< netcoreapp2.0) (< netstandard2.1)) (&& (== net7.0) (< netstandard1.1)) (&& (== net7.0) (< netstandard2.0)) (&& (== net7.0) (>= uap10.1)) (&& (== net8.0) (< netcoreapp2.0) (< netstandard2.1)) (&& (== net8.0) (< netstandard1.1)) (&& (== net8.0) (< netstandard2.0)) (&& (== net8.0) (>= uap10.1)) (== netstandard2.0) (&& (== netstandard2.1) (< netstandard1.1)) (&& (== netstandard2.1) (< netstandard2.0)) (&& (== netstandard2.1) (>= uap10.1)) System.Reflection.Emit.ILGeneration (4.7) - restriction: || (&& (== net7.0) (< netcoreapp2.0) (< netstandard2.1)) (&& (== net7.0) (< netstandard1.1)) (&& (== net7.0) (< netstandard2.0)) (&& (== net7.0) (>= uap10.1)) (&& (== net8.0) (< netcoreapp2.0) (< netstandard2.1)) (&& (== net8.0) (< netstandard1.1)) (&& (== net8.0) (< netstandard2.0)) (&& (== net8.0) (>= uap10.1)) (== netstandard2.0) (&& (== netstandard2.1) (< netstandard1.1)) (&& (== netstandard2.1) (< netstandard2.0)) (&& (== netstandard2.1) (>= uap10.1)) @@ -439,19 +577,142 @@ NUGET System.Reflection.Metadata (7.0) System.Collections.Immutable (>= 7.0) System.Memory (>= 4.5.5) - restriction: || (&& (== net6.0) (>= net462)) (&& (== net7.0) (>= net462)) (&& (== net7.0) (< net6.0)) (&& (== net8.0) (>= net462)) (&& (== net8.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) + System.Reflection.Primitives (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) System.Resources.Extensions (6.0) - copy_local: false System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< net6.0)) (&& (== net8.0) (>= net461)) (&& (== net8.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) + System.Resources.ResourceManager (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Globalization (>= 4.3) + System.Reflection (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime (4.3.1) + Microsoft.NETCore.Platforms (>= 1.1.1) + Microsoft.NETCore.Targets (>= 1.1.3) System.Runtime.CompilerServices.Unsafe (6.0) + System.Runtime.Extensions (4.3.1) + Microsoft.NETCore.Platforms (>= 1.1.1) + Microsoft.NETCore.Targets (>= 1.1.3) + System.Runtime (>= 4.3.1) + System.Runtime.Handles (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Runtime.InteropServices (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Reflection (>= 4.3) + System.Reflection.Primitives (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Handles (>= 4.3) + System.Runtime.Numerics (4.3) + System.Globalization (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) System.Security.AccessControl (6.0) - copy_local: false System.Security.Principal.Windows (>= 5.0) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< net6.0)) (&& (== net8.0) (>= net461)) (&& (== net8.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) - System.Security.Cryptography.Cng (5.0) - restriction: || (&& (== net6.0) (< netcoreapp3.1)) (&& (== net6.0) (< netstandard2.1)) (&& (== net7.0) (< net6.0)) (&& (== net7.0) (< netcoreapp3.1)) (&& (== net7.0) (< netstandard2.1)) (&& (== net8.0) (< net6.0)) (&& (== net8.0) (< netcoreapp3.1)) (&& (== net8.0) (< netstandard2.1)) (== netstandard2.0) (== netstandard2.1) + System.Security.Cryptography.Algorithms (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + runtime.native.System.Security.Cryptography.Apple (>= 4.3) + runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3) + System.Collections (>= 4.3) + System.IO (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Runtime.Handles (>= 4.3) + System.Runtime.InteropServices (>= 4.3) + System.Runtime.Numerics (>= 4.3) + System.Security.Cryptography.Encoding (>= 4.3) + System.Security.Cryptography.Primitives (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Security.Cryptography.Cng (5.0) + System.Security.Cryptography.Csp (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + System.IO (>= 4.3) + System.Reflection (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Runtime.Handles (>= 4.3) + System.Runtime.InteropServices (>= 4.3) + System.Security.Cryptography.Algorithms (>= 4.3) + System.Security.Cryptography.Encoding (>= 4.3) + System.Security.Cryptography.Primitives (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Threading (>= 4.3) + System.Security.Cryptography.Encoding (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3) + System.Collections (>= 4.3) + System.Collections.Concurrent (>= 4.3) + System.Linq (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Runtime.Handles (>= 4.3) + System.Runtime.InteropServices (>= 4.3) + System.Security.Cryptography.Primitives (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Security.Cryptography.OpenSsl (4.3) + runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3) + System.Collections (>= 4.3) + System.IO (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Runtime.Handles (>= 4.3) + System.Runtime.InteropServices (>= 4.3) + System.Runtime.Numerics (>= 4.3) + System.Security.Cryptography.Algorithms (>= 4.3) + System.Security.Cryptography.Encoding (>= 4.3) + System.Security.Cryptography.Primitives (>= 4.3) + System.Text.Encoding (>= 4.3) System.Security.Cryptography.Pkcs (6.0.4) System.Buffers (>= 4.5.1) - restriction: || (&& (== net6.0) (< netstandard2.1)) (&& (== net7.0) (< netstandard2.1)) (&& (== net8.0) (< netstandard2.1)) (== netstandard2.0) System.Formats.Asn1 (>= 6.0) System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (< netstandard2.1)) (&& (== net7.0) (< netstandard2.1)) (&& (== net8.0) (< netstandard2.1)) (== netstandard2.0) System.Security.Cryptography.Cng (>= 5.0) - restriction: || (&& (== net6.0) (< netcoreapp3.1)) (&& (== net6.0) (< netstandard2.1)) (&& (== net7.0) (< net6.0)) (&& (== net7.0) (< netcoreapp3.1)) (&& (== net7.0) (< netstandard2.1)) (&& (== net8.0) (< net6.0)) (&& (== net8.0) (< netcoreapp3.1)) (&& (== net8.0) (< netstandard2.1)) (== netstandard2.0) (== netstandard2.1) - System.Security.Cryptography.ProtectedData (6.0) + System.Security.Cryptography.Primitives (4.3) + System.Diagnostics.Debug (>= 4.3) + System.Globalization (>= 4.3) + System.IO (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Threading (>= 4.3) + System.Threading.Tasks (>= 4.3) + System.Security.Cryptography.ProtectedData (6.0) - copy_local: false System.Memory (>= 4.5.4) - restriction: || (&& (== net7.0) (< net6.0)) (&& (== net8.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) + System.Security.Cryptography.X509Certificates (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + runtime.native.System (>= 4.3) + runtime.native.System.Net.Http (>= 4.3) + runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3) + System.Collections (>= 4.3) + System.Diagnostics.Debug (>= 4.3) + System.Globalization (>= 4.3) + System.Globalization.Calendars (>= 4.3) + System.IO (>= 4.3) + System.IO.FileSystem (>= 4.3) + System.IO.FileSystem.Primitives (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Runtime.Handles (>= 4.3) + System.Runtime.InteropServices (>= 4.3) + System.Runtime.Numerics (>= 4.3) + System.Security.Cryptography.Algorithms (>= 4.3) + System.Security.Cryptography.Cng (>= 4.3) + System.Security.Cryptography.Csp (>= 4.3) + System.Security.Cryptography.Encoding (>= 4.3) + System.Security.Cryptography.OpenSsl (>= 4.3) + System.Security.Cryptography.Primitives (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Threading (>= 4.3) System.Security.Cryptography.Xml (6.0.1) - copy_local: false System.Memory (>= 4.5.4) - restriction: || (&& (== net7.0) (< net6.0)) (&& (== net8.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) System.Security.AccessControl (>= 6.0) @@ -460,17 +721,37 @@ NUGET System.Security.AccessControl (>= 6.0) - restriction: || (&& (== net6.0) (>= net462)) (&& (== net7.0) (>= net462)) (&& (== net7.0) (< net6.0)) (&& (== net8.0) (>= net462)) (&& (== net8.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) System.Windows.Extensions (>= 7.0) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) System.Security.Principal.Windows (5.0) - copy_local: false + System.Text.Encoding (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) System.Text.Encoding.CodePages (6.0) - copy_local: false System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netcoreapp3.1)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< netcoreapp3.1)) (&& (== net8.0) (>= net461)) (&& (== net8.0) (< netcoreapp3.1)) (== netstandard2.0) (== netstandard2.1) System.Runtime.CompilerServices.Unsafe (>= 6.0) - System.Text.Encodings.Web (6.0) - copy_local: false, restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net472)) (&& (== netstandard2.1) (>= net6.0)) - System.Runtime.CompilerServices.Unsafe (>= 6.0) - System.Text.Json (6.0.5) - copy_local: false, restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net472)) (&& (== netstandard2.1) (>= net6.0)) - System.Runtime.CompilerServices.Unsafe (>= 6.0) - System.Text.Encodings.Web (>= 6.0) + System.Text.Encodings.Web (7.0) + System.Buffers (>= 4.5.1) - restriction: || (&& (== net6.0) (>= net462)) (&& (== net7.0) (>= net462)) (&& (== net7.0) (< net6.0)) (&& (== net8.0) (>= net462)) (&& (== net8.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) + System.Memory (>= 4.5.5) - restriction: || (&& (== net6.0) (>= net462)) (&& (== net7.0) (>= net462)) (&& (== net7.0) (< net6.0)) (&& (== net8.0) (>= net462)) (&& (== net8.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) + System.Runtime.CompilerServices.Unsafe (>= 6.0) - restriction: || (== net6.0) (&& (== net7.0) (>= net462)) (&& (== net7.0) (< net6.0)) (&& (== net8.0) (>= net462)) (&& (== net8.0) (< net6.0)) (&& (== net8.0) (< net7.0)) (== netstandard2.0) (== netstandard2.1) + System.Text.Json (7.0.3) - copy_local: false + System.Runtime.CompilerServices.Unsafe (>= 6.0) - restriction: || (== net6.0) (&& (== net7.0) (>= net462)) (&& (== net7.0) (< net6.0)) (&& (== net8.0) (>= net462)) (&& (== net8.0) (< net6.0)) (&& (== net8.0) (< net7.0)) (== netstandard2.0) (== netstandard2.1) + System.Text.Encodings.Web (>= 7.0) + System.Text.RegularExpressions (4.3.1) + System.Collections (>= 4.3) - restriction: || (&& (== net6.0) (< netcoreapp1.1)) (&& (== net7.0) (< netcoreapp1.1)) (&& (== net8.0) (< netcoreapp1.1)) (== netstandard2.0) (== netstandard2.1) + System.Globalization (>= 4.3) - restriction: || (&& (== net6.0) (< netcoreapp1.1)) (&& (== net7.0) (< netcoreapp1.1)) (&& (== net8.0) (< netcoreapp1.1)) (== netstandard2.0) (== netstandard2.1) + System.Resources.ResourceManager (>= 4.3) - restriction: || (&& (== net6.0) (< netcoreapp1.1)) (&& (== net7.0) (< netcoreapp1.1)) (&& (== net8.0) (< netcoreapp1.1)) (== netstandard2.0) (== netstandard2.1) + System.Runtime (>= 4.3.1) + System.Runtime.Extensions (>= 4.3.1) - restriction: || (&& (== net6.0) (< netcoreapp1.1)) (&& (== net7.0) (< netcoreapp1.1)) (&& (== net8.0) (< netcoreapp1.1)) (== netstandard2.0) (== netstandard2.1) + System.Threading (>= 4.3) - restriction: || (&& (== net6.0) (< netcoreapp1.1)) (&& (== net7.0) (< netcoreapp1.1)) (&& (== net8.0) (< netcoreapp1.1)) (== netstandard2.0) (== netstandard2.1) + System.Threading (4.3) + System.Runtime (>= 4.3) + System.Threading.Tasks (>= 4.3) System.Threading.Channels (6.0) System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netstandard2.1)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< netstandard2.1)) (&& (== net8.0) (>= net461)) (&& (== net8.0) (< netstandard2.1)) (== netstandard2.0) (&& (== netstandard2.1) (>= net461)) - System.Threading.Tasks.Dataflow (6.0) - copy_local: false + System.Threading.Tasks (4.3) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Threading.Tasks.Dataflow (7.0) - copy_local: false System.Threading.Tasks.Extensions (4.5.4) System.Runtime.CompilerServices.Unsafe (>= 4.5.3) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netcoreapp2.1)) (&& (== net6.0) (< netstandard1.0)) (&& (== net6.0) (< netstandard2.0)) (&& (== net6.0) (>= wp8)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< netcoreapp2.1)) (&& (== net7.0) (< netstandard1.0)) (&& (== net7.0) (< netstandard2.0)) (&& (== net7.0) (>= wp8)) (&& (== net8.0) (>= net461)) (&& (== net8.0) (< netcoreapp2.1)) (&& (== net8.0) (< netstandard1.0)) (&& (== net8.0) (< netstandard2.0)) (&& (== net8.0) (>= wp8)) (== netstandard2.0) (== netstandard2.1) System.Windows.Extensions (7.0) - copy_local: false, restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) @@ -480,8 +761,8 @@ NUGET FSharp.Core (>= 7.0.200) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) System.Collections.Immutable (>= 6.0) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) remote: https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8/nuget/v3/index.json - FSharp.Compiler.Service (43.8.200-preview.23562.1) - FSharp.Core (8.0.200-beta.23562.1) + FSharp.Compiler.Service (43.8.200-preview.24061.1) + FSharp.Core (8.0.200-beta.24061.1) System.Buffers (>= 4.5.1) System.Collections.Immutable (>= 7.0) System.Diagnostics.DiagnosticSource (>= 7.0.2) @@ -489,7 +770,7 @@ NUGET System.Reflection.Emit (>= 4.7) System.Reflection.Metadata (>= 7.0) System.Runtime.CompilerServices.Unsafe (>= 6.0) - FSharp.Core (8.0.200-beta.23562.1) + FSharp.Core (8.0.200-beta.24061.1) GROUP Build STORAGE: NONE diff --git a/src/FsAutoComplete.Core/AbstractClassStubGenerator.fs b/src/FsAutoComplete.Core/AbstractClassStubGenerator.fs index 397636557..9266c09ac 100644 --- a/src/FsAutoComplete.Core/AbstractClassStubGenerator.fs +++ b/src/FsAutoComplete.Core/AbstractClassStubGenerator.fs @@ -35,7 +35,7 @@ let private (|ExplicitCtor|_|) = | _ -> None /// checks to see if a type definition inherits an abstract class, and if so collects the members defined at that -let private walkTypeDefn (SynTypeDefn(info, repr, members, implicitCtor, range, trivia)) = +let private walkTypeDefn (SynTypeDefn(_, repr, members, implicitCtor, _, _)) = option { let reprMembers = match repr with @@ -47,7 +47,7 @@ let private walkTypeDefn (SynTypeDefn(info, repr, members, implicitCtor, range, let! inheritType, inheritMemberRange = // this must exist for abstract types allMembers |> List.tryPick (function - | SynMemberDefn.ImplicitInherit(inheritType, inheritArgs, alias, range) -> Some(inheritType, range) + | SynMemberDefn.ImplicitInherit(inheritType, _, _, range) -> Some(inheritType, range) | _ -> None) let furthestMemberToSkip, otherMembers = @@ -82,13 +82,14 @@ let private tryFindAbstractClassExprInParsedInput { new SyntaxVisitorBase<_>() with member _.VisitExpr(path, traverseExpr, defaultTraverse, expr) = match expr with - | SynExpr.ObjExpr(baseTy, constructorArgs, withKeyword, bindings, members, extraImpls, newExprRange, range) -> + | SynExpr.ObjExpr( + objType = baseTy; withKeyword = withKeyword; bindings = bindings; newExprRange = newExprRange) -> Some(AbstractClassData.ObjExpr(baseTy, bindings, newExprRange, withKeyword)) | _ -> defaultTraverse expr override _.VisitModuleDecl(_, defaultTraverse, decl) = match decl with - | SynModuleDecl.Types(types, m) -> List.tryPick walkTypeDefn types + | SynModuleDecl.Types(types, _) -> List.tryPick walkTypeDefn types | _ -> defaultTraverse decl } ) @@ -104,12 +105,12 @@ let tryFindAbstractClassExprInBufferAtPos return! tryFindAbstractClassExprInParsedInput pos parseResults.ParseTree } -let getMemberNameAndRanges (abstractClassData) = +let getMemberNameAndRanges abstractClassData = match abstractClassData with | AbstractClassData.ExplicitImpl(members = members) -> members |> Seq.choose (function - | (SynMemberDefn.Member(binding, _)) -> Some binding + | SynMemberDefn.Member(binding, _) -> Some binding | _ -> None) |> Seq.choose (|MemberNamePlusRangeAndKeywordRange|_|) |> Seq.toList @@ -130,7 +131,7 @@ let inferStartColumn | AbstractClassData.ExplicitImpl(inheritExpressionRange = inheritRange) -> // 'interface ISomething with' is often in a new line, we use the indentation of that line inheritRange.StartColumn - | AbstractClassData.ObjExpr(newExpression = newExpr; withKeyword = withKeyword; bindings = bindings) -> + | AbstractClassData.ObjExpr(newExpression = newExpr; withKeyword = withKeyword; bindings = _) -> // two cases here to consider: // * has a with keyword on same line as newExpr match withKeyword with @@ -153,19 +154,19 @@ let writeAbstractClassStub (codeGenServer: ICodeGenerationService) (checkResultForFile: ParseAndCheckResults) (doc: IFSACSourceText) - (lineStr: string) + (_: string) (abstractClassData: AbstractClassData) = asyncOption { let pos = Position.mkPos abstractClassData.AbstractTypeIdentRange.Start.Line - (abstractClassData.AbstractTypeIdentRange.End.Column) + abstractClassData.AbstractTypeIdentRange.End.Column - let! (_lexerSym, usages) = codeGenServer.GetSymbolAndUseAtPositionOfKind(doc.FileName, pos, SymbolKind.Ident) + let! _lexerSym, usages = codeGenServer.GetSymbolAndUseAtPositionOfKind(doc.FileName, pos, SymbolKind.Ident) let! usage = usages - let! (displayContext, entity) = + let! displayContext, entity = asyncOption { // need the enclosing entity because we're always looking at a ctor, which isn't an Entity, but a MemberOrFunctionOrValue match usage.Symbol with @@ -178,7 +179,7 @@ let writeAbstractClassStub | _ -> return! None } - let getMemberByLocation (name: string, range: Range, keywordRange: Range) = + let getMemberByLocation (_: string, range: Range, _: Range) = match doc.GetLine range.Start with | Some lineText -> match Lexer.getSymbol range.Start.Line range.Start.Column lineText SymbolLookupKind.ByLongIdent [||] with diff --git a/src/FsAutoComplete.Core/AdaptiveExtensions.fs b/src/FsAutoComplete.Core/AdaptiveExtensions.fs index a70e9830b..26820e83d 100644 --- a/src/FsAutoComplete.Core/AdaptiveExtensions.fs +++ b/src/FsAutoComplete.Core/AdaptiveExtensions.fs @@ -28,6 +28,20 @@ module AdaptiveExtensions = with _ -> () + type TaskCompletionSource<'a> with + + /// https://github.com/dotnet/runtime/issues/47998 + member tcs.TrySetFromTask(real: Task<'a>) = + task { + try + let! r = real + tcs.TrySetResult r |> ignore + with + | :? OperationCanceledException as x -> tcs.TrySetCanceled(x.CancellationToken) |> ignore + | ex -> tcs.TrySetException ex |> ignore + } + |> ignore> + type ChangeableHashMap<'Key, 'Value> with /// @@ -43,7 +57,7 @@ module AdaptiveExtensions = /// /// Adds the given key and calls the adder function if no previous key exists. /// Otherwise calls updater with the current key/value but does not override existing value in the map. - /// This is useful when the 'Value is itself a changeable value like a cval, aset, amap which should be changed + /// This is useful when the 'Value is itself a changeable value like a cval, cset, cmap which should be changed /// but the parent container doesn't need to know about those changes itself. /// member x.AddOrElse(key, adder, updater) = @@ -77,7 +91,7 @@ type MapDisposableTupleVal<'T1, 'T2, 'Disposable when 'Disposable :> IDisposable match cache with | ValueSome(struct (a, b, _)) when Utils.cheapEqual a i -> b - | ValueSome(struct (a, b, c)) -> + | ValueSome(struct (_, _, c)) -> (c :> IDisposable).Dispose() let (b, c) = mapping i cache <- ValueSome(struct (i, b, c)) @@ -209,7 +223,7 @@ module AMap = dirty <- HashMap.empty d) - override x.InputChangedObject(t, o) = + override x.InputChangedObject(_, o) = #if FABLE_COMPILER if isNull o.Tag then let o = unbox> o @@ -316,7 +330,7 @@ module AMap = = let mapping = mapping - >> HashMap.map (fun _ v -> AVal.constant v |> AVal.mapWithAdditionalDependencies (id)) + >> HashMap.map (fun _ v -> AVal.constant v |> AVal.mapWithAdditionalDependencies id) batchRecalcDirty mapping map @@ -366,33 +380,44 @@ type internal RefCountingTaskCreator<'a>(create: CancellationToken -> Task<'a>) /// and AdaptiveCancellableTask<'a>(cancel: unit -> unit, real: Task<'a>) = let cts = new CancellationTokenSource() + let mutable cached: Task<'a> = null - let output = - if real.IsCompleted then - real - else - let tcs = new TaskCompletionSource<'a>() + let getTask () = + let createCached () = + if real.IsCompleted then + real + else + task { + let tcs = new TaskCompletionSource<'a>() + use _s = cts.Token.Register(fun () -> tcs.TrySetCanceled(cts.Token) |> ignore) - let s = cts.Token.Register(fun () -> tcs.TrySetCanceled() |> ignore) + tcs.TrySetFromTask real - real.ContinueWith(fun (t: Task<'a>) -> - s.Dispose() + return! tcs.Task + } - if t.IsFaulted then tcs.TrySetException(t.Exception) - elif t.IsCanceled then tcs.TrySetCanceled() - else tcs.TrySetResult(t.Result)) - |> ignore + cached <- + match cached with + | null -> createCached () + | x when x.IsCompleted && not real.IsCompleted -> + // When the real task isn't finished, we create a new task to attach to the real task + // so we can cancel this new task immediately without waiting for the real task to cancel (as other tasks might depend on it and we use ref counting) + // However, if the cached task is completed (cancelled or faulted) but the real task is not, + // we should re-attach to the original task instead of assuming we want to cache the cancelled/faulted task. + createCached () + | o -> o - tcs.Task + cached /// Will run the cancel function passed into the constructor and set the output Task to cancelled state. member x.Cancel() = - cancel () - cts.TryCancel() + lock x (fun () -> + cancel () + cts.TryCancel()) /// The output of the passed in task to the constructor. /// - member x.Task = output + member x.Task = lock x getTask type asyncaval<'a> = inherit IAdaptiveObject @@ -416,10 +441,10 @@ module Async = return! ct.Task |> Async.AwaitTask } -[] +[] module Extensions = - type IcedTasks.CancellableTasks.CancellableTaskBuilderBase with + type IcedTasks.CancellableTaskBase.CancellableTaskBuilderBase with /// Allows implicit conversion of a AdaptiveCancellableTask to a CancellableTask in a cancellableTask CE. member inline x.Source(ct: AdaptiveCancellableTask<_>) = @@ -575,10 +600,7 @@ module AsyncAVal = let ref = RefCountingTaskCreator( cancellableTask { - let! ct = CancellableTask.getCancellationToken () - let it = input.GetValue t - use _s = ct.Register(fun () -> it.Cancel()) - let! i = it + let! i = input.GetValue t return! mapping i } ) @@ -633,8 +655,8 @@ module AsyncAVal = ta.Cancel() tb.Cancel()) - let! va = ta - let! vb = tb + let! va = ta.Task + let! vb = tb.Task return! mapping va vb } ) @@ -672,11 +694,8 @@ module AsyncAVal = let outerTask = RefCountingTaskCreator( cancellableTask { - let it = value.GetValue t + let! i = value.GetValue t let! ct = CancellableTask.getCancellationToken () - use _s = ct.Register(fun () -> it.Cancel()) - - let! i = it let inner = mapping i ct return inner @@ -690,14 +709,11 @@ module AsyncAVal = let ref = RefCountingTaskCreator( cancellableTask { - let innerCellTask = outerTask.New() - let! ct = CancellableTask.getCancellationToken () - use _s = ct.Register(fun () -> innerCellTask.Cancel()) - let! inner = innerCellTask - let innerTask = inner.GetValue t + let! inner = outerTask.New() lock inners (fun () -> inners.Value <- HashSet.add inner inners.Value) + let innerTask = inner.GetValue t use _s2 = ct.Register(fun () -> @@ -705,7 +721,7 @@ module AsyncAVal = lock inners (fun () -> inners.Value <- HashSet.remove inner inners.Value) inner.Outputs.Remove x |> ignore) - return! innerTask + return! innerTask.Task } ) diff --git a/src/FsAutoComplete.Core/AdaptiveExtensions.fsi b/src/FsAutoComplete.Core/AdaptiveExtensions.fsi index c5f7bf724..0c725f3c3 100644 --- a/src/FsAutoComplete.Core/AdaptiveExtensions.fsi +++ b/src/FsAutoComplete.Core/AdaptiveExtensions.fsi @@ -16,7 +16,7 @@ module AdaptiveExtensions = /// /// Adds the given key and calls the adder function if no previous key exists. /// Otherwise calls updater with the current key/value but does not override existing value in the map. - /// This is useful when the 'Value is itself a changeable value like a cval, aset, amap which should be changed + /// This is useful when the 'Value is itself a changeable value like a cval, cset, cmap which should be changed /// but the parent container doesn't need to know about those changes itself. /// member AddOrElse: key: 'Key * adder: ('Key -> 'Value) * updater: ('Key -> 'Value -> unit) -> unit @@ -191,7 +191,7 @@ module Async = [] module Extensions = - type IcedTasks.CancellableTasks.CancellableTaskBuilderBase with + type IcedTasks.CancellableTaskBase.CancellableTaskBuilderBase with /// Allows implicit conversion of a AdaptiveCancellableTask to a CancellableTask in a cancellableTask CE. member inline Source: diff --git a/src/FsAutoComplete.Core/CodeGeneration.fs b/src/FsAutoComplete.Core/CodeGeneration.fs index e5b37638b..49a3706d2 100644 --- a/src/FsAutoComplete.Core/CodeGeneration.fs +++ b/src/FsAutoComplete.Core/CodeGeneration.fs @@ -302,8 +302,10 @@ module CodeGenerationUtils = let displayName = v.DisplayName if - (v.IsPropertyGetterMethod && displayName.StartsWith("get_")) - || (v.IsPropertySetterMethod && displayName.StartsWith("set_")) + (v.IsPropertyGetterMethod + && displayName.StartsWith("get_", StringComparison.Ordinal)) + || (v.IsPropertySetterMethod + && displayName.StartsWith("set_", StringComparison.Ordinal)) then displayName.[4..] else @@ -328,7 +330,7 @@ module CodeGenerationUtils = (if String.IsNullOrWhiteSpace(args) then "" - elif args.StartsWith("(") then + elif args.StartsWith("(", StringComparison.Ordinal) then args elif v.CurriedParameterGroups.Count > 1 && (not verboseMode) then " " + args @@ -345,7 +347,10 @@ module CodeGenerationUtils = | _, _, ".ctor", _ -> "new" + parArgs // Properties (skipping arguments) | _, true, _, name when v.IsPropertyGetterMethod || v.IsPropertySetterMethod -> - if name.StartsWith("get_") || name.StartsWith("set_") then + if + name.StartsWith("get_", StringComparison.Ordinal) + || name.StartsWith("set_", StringComparison.Ordinal) + then name.[4..] else name @@ -549,7 +554,7 @@ module CodeGenerationUtils = let getAbstractNonVirtualMembers (e: FSharpEntity) = seq { - let genericParams = e.GenericParameters :> seq<_> + let _genericParams = e.GenericParameters :> seq<_> // todo: generic param instantiations? yield! e.MembersFunctionsAndValues @@ -576,14 +581,14 @@ module CodeGenerationUtils = | SynBinding(valData = SynValData(memberFlags = Some mf); headPat = LongIdentPattern(name, range); trivia = trivia) when mf.MemberKind = SynMemberKind.PropertyGet -> - if name.StartsWith("get_") then + if name.StartsWith("get_", StringComparison.Ordinal) then Some(name, range, trivia.LeadingKeyword.Range) else Some("get_" + name, range, trivia.LeadingKeyword.Range) | SynBinding(valData = SynValData(memberFlags = Some mf); headPat = LongIdentPattern(name, range); trivia = trivia) when mf.MemberKind = SynMemberKind.PropertySet -> - if name.StartsWith("set_") then + if name.StartsWith("set_", StringComparison.Ordinal) then Some(name, range, trivia.LeadingKeyword.Range) else Some("set_" + name, range, trivia.LeadingKeyword.Range) @@ -594,9 +599,12 @@ module CodeGenerationUtils = let normalizeEventName (m: FSharpMemberOrFunctionOrValue) = let name = m.DisplayName - if name.StartsWith("add_") then name.[4..] - elif name.StartsWith("remove_") then name.[7..] - else name + if name.StartsWith("add_", StringComparison.Ordinal) then + name.[4..] + elif name.StartsWith("remove_", StringComparison.Ordinal) then + name.[7..] + else + name /// Ideally this info should be returned in error symbols from FCS. /// Because it isn't, we implement a crude way of getting member signatures: diff --git a/src/FsAutoComplete.Core/Commands.fs b/src/FsAutoComplete.Core/Commands.fs index 0c2856a17..580599657 100644 --- a/src/FsAutoComplete.Core/Commands.fs +++ b/src/FsAutoComplete.Core/Commands.fs @@ -248,7 +248,6 @@ module Commands = (tyRes: ParseAndCheckResults) (pos: Position) (lines: IFSACSourceText) - (line: LineStr) = async { let doc = docForText lines tyRes @@ -309,7 +308,7 @@ module Commands = if (e.Kind LookupType.Fuzzy) = EntityKind.Attribute - && lastIdent.EndsWith "Attribute" + && lastIdent.EndsWith("Attribute", StringComparison.Ordinal) then yield e.TopRequireQualifiedAccessParent, @@ -363,7 +362,6 @@ module Commands = (tyRes: ParseAndCheckResults) (pos: Position) (lines: ISourceText) - (line: LineStr) = async { @@ -534,9 +532,7 @@ module Commands = | Error e -> return CoreResponse.ErrorRes e } - let typesig (tyRes: ParseAndCheckResults) (pos: Position) lineStr = - tyRes.TryGetToolTip pos lineStr - |> Result.bimap CoreResponse.Res CoreResponse.ErrorRes + let typesig (tyRes: ParseAndCheckResults) (pos: Position) lineStr = tyRes.TryGetToolTip pos lineStr // Calculates pipeline hints for now as in fSharp/pipelineHint with a bit of formatting on the hints let inlineValues (contents: IFSACSourceText) (tyRes: ParseAndCheckResults) : Async<(pos * String)[]> = @@ -546,7 +542,7 @@ module Commands = option { let! lineStr = contents.GetLine pos - let! tip = tyRes.TryGetToolTip pos lineStr |> Option.ofResult + let! tip = tyRes.TryGetToolTip pos lineStr return TipFormatter.extractGenericParameters tip } @@ -562,8 +558,8 @@ module Commands = let getStartingPipe = function - | y :: xs when y.TokenName.ToUpper() = "INFIX_BAR_OP" -> Some y - | x :: y :: xs when x.TokenName.ToUpper() = "WHITESPACE" && y.TokenName.ToUpper() = "INFIX_BAR_OP" -> Some y + | y :: _ when y.TokenName.ToUpper() = "INFIX_BAR_OP" -> Some y + | x :: y :: _ when x.TokenName.ToUpper() = "WHITESPACE" && y.TokenName.ToUpper() = "INFIX_BAR_OP" -> Some y | _ -> None let folder (lastExpressionLine, lastExpressionLineWasPipe, acc) (currentIndex, currentTokens) = @@ -618,7 +614,7 @@ module Commands = option { let! lineStr = contents.GetLine pos - let! tip = tyRes.TryGetToolTip pos lineStr |> Option.ofResult + let! tip = tyRes.TryGetToolTip pos lineStr return TipFormatter.extractGenericParameters tip } @@ -634,8 +630,8 @@ module Commands = let getStartingPipe = function - | y :: xs when y.TokenName.ToUpper() = "INFIX_BAR_OP" -> Some y - | x :: y :: xs when x.TokenName.ToUpper() = "WHITESPACE" && y.TokenName.ToUpper() = "INFIX_BAR_OP" -> Some y + | y :: _ when y.TokenName.ToUpper() = "INFIX_BAR_OP" -> Some y + | x :: y :: _ when x.TokenName.ToUpper() = "WHITESPACE" && y.TokenName.ToUpper() = "INFIX_BAR_OP" -> Some y | _ -> None let folder (lastExpressionLine, lastExpressionLineWasPipe, acc) (currentIndex, currentTokens) = @@ -706,7 +702,7 @@ module Commands = |> Seq.tryFind (fun l -> let lineStr = getLine l // namespace MUST be top level -> no indentation - lineStr.StartsWith "namespace ") + lineStr.StartsWith("namespace ", StringComparison.Ordinal)) |> function // move to the next line below "namespace" | Some l -> l.IncLine() @@ -716,7 +712,7 @@ module Commands = // adjust column let pos = match pos with - | Pos(1, c) -> pos + | Pos(1, _) -> pos | Pos(l, 0) -> let prev = getLine (pos.DecLine()) let indentation = detectIndentation prev @@ -726,7 +722,7 @@ module Commands = Position.mkPos l indentation else pos - | Pos(_, c) -> pos + | Pos(_, _) -> pos { Namespace = n Position = pos @@ -795,7 +791,6 @@ module Commands = match scope with | Some(SymbolDeclarationLocation.Projects(projects (*isLocalForProject=*) , true)) -> return projects | Some(SymbolDeclarationLocation.Projects(projects (*isLocalForProject=*) , false)) -> - let output = ResizeArray<_>() let! resolvedProjects = [ for project in projects do @@ -979,7 +974,7 @@ module Commands = /// /// Also does very basic validation of `newName`: /// * Must be valid operator name when operator - let adjustRenameSymbolNewName pos lineStr (text: IFSACSourceText) (tyRes: ParseAndCheckResults) (newName: string) = + let adjustRenameSymbolNewName pos lineStr (tyRes: ParseAndCheckResults) (newName: string) = asyncResult { let! symbolUse = tyRes.TryGetSymbolUse pos lineStr @@ -1000,7 +995,10 @@ module Commands = // -> only check if no backticks let newBacktickedName = newName |> PrettyNaming.NormalizeIdentifierBackticks - if newBacktickedName.StartsWith "``" && newBacktickedName.EndsWith "``" then + if + newBacktickedName.StartsWith("``", StringComparison.Ordinal) + && newBacktickedName.EndsWith("``", StringComparison.Ordinal) + then return newBacktickedName elif PrettyNaming.IsIdentifierName newName then return newName @@ -1139,14 +1137,22 @@ module Commands = - let analyzerHandler (file: string, content, pt, tast, symbols, getAllEntities) = - let ctx: SDK.Context = + let analyzerHandler + ( + client: SDK.Client, + file: string, + content: ISourceText, + pt, + tast, + checkFileResults: FSharpCheckFileResults + ) = + let ctx: SDK.EditorContext = { FileName = UMX.untag file - Content = content - ParseTree = pt - TypedTree = tast - Symbols = symbols - GetAllEntities = getAllEntities } + SourceText = content + ParseFileResults = pt + CheckFileResults = Some checkFileResults + TypedTree = Some tast + CheckProjectResults = None } let extractResultsFromAnalyzer (r: SDK.AnalysisResult) = match r.Output with @@ -1170,20 +1176,20 @@ module Commands = [] - try - SDK.Client.runAnalyzersSafely ctx - |> List.collect extractResultsFromAnalyzer - |> List.toArray - with ex -> - Loggers.analyzers.error ( - Log.setMessage "Error while processing analyzers for {file}: {message}" - >> Log.addContextDestructured "message" ex.Message - >> Log.addExn ex - >> Log.addContextDestructured "file" file - ) - - [||] + async { + try + let! r = client.RunAnalyzersSafely ctx + return r |> List.collect extractResultsFromAnalyzer |> List.toArray + with ex -> + Loggers.analyzers.error ( + Log.setMessage "Error while processing analyzers for {file}: {message}" + >> Log.addContextDestructured "message" ex.Message + >> Log.addExn ex + >> Log.addContextDestructured "file" file + ) + return [||] + } type Commands() = diff --git a/src/FsAutoComplete.Core/CompilerServiceInterface.fs b/src/FsAutoComplete.Core/CompilerServiceInterface.fs index a75bf8fb9..4fa32b589 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fs +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fs @@ -65,17 +65,17 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe | None, _ | _, None -> p | Some fsc, Some fsi -> - let toReplace, otherOpts = + let _toReplace, otherOpts = p.OtherOptions |> Array.partition (fun opt -> - opt.EndsWith "FSharp.Core.dll" - || opt.EndsWith "FSharp.Compiler.Interactive.Settings.dll") + opt.EndsWith("FSharp.Core.dll", StringComparison.Ordinal) + || opt.EndsWith("FSharp.Compiler.Interactive.Settings.dll", StringComparison.Ordinal)) { p with OtherOptions = Array.append otherOpts [| $"-r:%s{fsc.FullName}"; $"-r:%s{fsi.FullName}" |] } let (|StartsWith|_|) (prefix: string) (s: string) = - if s.StartsWith(prefix) then + if s.StartsWith(prefix, StringComparison.Ordinal) then Some(s.[prefix.Length ..]) else None @@ -94,20 +94,6 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe else opts - let filterBadRuntimeRefs = - let badRefs = - [ "System.Private" - "System.Runtime.WindowsRuntime" - "System.Runtime.WindowsRuntime.UI.Xaml" - "mscorlib" ] - |> List.map (fun p -> p + ".dll") - - let containsBadRef (s: string) = badRefs |> List.exists (fun r -> s.EndsWith r) - - fun (projOptions: FSharpProjectOptions) -> - { projOptions with - OtherOptions = projOptions.OtherOptions |> Array.where (containsBadRef >> not) } - /// ensures that any user-configured include/load files are added to the typechecking context let addLoadedFiles (projectOptions: FSharpProjectOptions) = let files = Array.append fsiAdditionalFiles projectOptions.SourceFiles @@ -120,7 +106,11 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe { projectOptions with SourceFiles = files } - let (|Reference|_|) (opt: string) = if opt.StartsWith "-r:" then Some(opt.[3..]) else None + let (|Reference|_|) (opt: string) = + if opt.StartsWith("-r:", StringComparison.Ordinal) then + Some(opt.[3..]) + else + None /// ensures that all file paths are absolute before being sent to the compiler, because compilation of scripts fails with relative paths let resolveRelativeFilePaths (projectOptions: FSharpProjectOptions) = @@ -432,7 +422,7 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe ) } - member __.GetDeclarations(fileName: string, source, options, version) = + member __.GetDeclarations(fileName: string, source, options, _) = async { checkerLogger.info ( Log.setMessage "GetDeclarations - {file}" diff --git a/src/FsAutoComplete.Core/Debug.fs b/src/FsAutoComplete.Core/Debug.fs index aee09eaf5..5cce1516b 100644 --- a/src/FsAutoComplete.Core/Debug.fs +++ b/src/FsAutoComplete.Core/Debug.fs @@ -48,7 +48,7 @@ module Debug = return r } - let toggleVerboseLogging (verbose: bool) = () // todo: set logging latch + let toggleVerboseLogging (_verbose: bool) = () // todo: set logging latch let waitForDebugger () = while not (Diagnostics.Debugger.IsAttached) do @@ -56,7 +56,13 @@ module Debug = let logger = LogProvider.getLoggerByName "Debugging" - let waitForDebuggerAttached (programName) = + let waitForDebuggerAttached +#if DEBUG + programName +#else + _ +#endif + = #if DEBUG if not (System.Diagnostics.Debugger.IsAttached) then logger.info ( @@ -73,7 +79,13 @@ module Debug = #else () #endif - let waitForDebuggerAttachedAndBreak (programName) = + let waitForDebuggerAttachedAndBreak +#if DEBUG + programName +#else + _ +#endif + = #if DEBUG waitForDebuggerAttached programName System.Diagnostics.Debugger.Break() diff --git a/src/FsAutoComplete.Core/DocumentationFormatter.fs b/src/FsAutoComplete.Core/DocumentationFormatter.fs index 6220d2cba..8423b0347 100644 --- a/src/FsAutoComplete.Core/DocumentationFormatter.fs +++ b/src/FsAutoComplete.Core/DocumentationFormatter.fs @@ -104,7 +104,7 @@ module DocumentationFormatter = let t = Regex.Replace(org, """<.*>""", "<") [ yield formatShowDocumentationLink t xmlDocSig assemblyName - if t.EndsWith "<" then + if t.EndsWith("<", StringComparison.Ordinal) then yield! renderedGenericArgumentTypes |> Seq.intersperse (", ", 2) yield formatShowDocumentationLink ">" xmlDocSig assemblyName ] @@ -217,7 +217,7 @@ module DocumentationFormatter = let typeList = unionCase.Fields |> Seq.map (fun unionField -> - if unionField.Name.StartsWith "Item" then //TODO: Some better way of detecting default names for the union cases' fields + if unionField.Name.StartsWith("Item", StringComparison.Ordinal) then //TODO: Some better way of detecting default names for the union cases' fields unionField.FieldType |> format displayContext |> fst else @@ -231,7 +231,7 @@ module DocumentationFormatter = unionCase.DisplayName let getFuncSignatureWithIdent displayContext (func: FSharpMemberOrFunctionOrValue) (ident: int) = - let maybeGetter = func.LogicalName.StartsWith "get_" + let maybeGetter = func.LogicalName.StartsWith("get_", StringComparison.Ordinal) let indent = String.replicate ident " " let functionName = @@ -243,7 +243,7 @@ module DocumentationFormatter = |> FSharpKeywords.NormalizeIdentifierBackticks elif func.IsOperatorOrActivePattern then func.DisplayName - elif func.DisplayName.StartsWith "( " then + elif func.DisplayName.StartsWith("( ", StringComparison.Ordinal) then FSharpKeywords.NormalizeIdentifierBackticks func.LogicalName else FSharpKeywords.NormalizeIdentifierBackticks func.DisplayName @@ -416,9 +416,12 @@ module DocumentationFormatter = "new" elif func.IsOperatorOrActivePattern then func.DisplayName - elif func.DisplayName.StartsWith "( " then + elif func.DisplayName.StartsWith("( ", StringComparison.Ordinal) then FSharpKeywords.NormalizeIdentifierBackticks func.LogicalName - elif func.LogicalName.StartsWith "get_" || func.LogicalName.StartsWith "set_" then + elif + func.LogicalName.StartsWith("get_", StringComparison.Ordinal) + || func.LogicalName.StartsWith("set_", StringComparison.Ordinal) + then PrettyNaming.TryChopPropertyName func.DisplayName |> Option.defaultValue func.DisplayName else @@ -536,7 +539,7 @@ module DocumentationFormatter = let prefix = if v.IsMutable then "val mutable" else "val" let name = - (if v.DisplayName.StartsWith "( " then + (if v.DisplayName.StartsWith("( ", StringComparison.Ordinal) then v.LogicalName else v.DisplayName) @@ -584,7 +587,7 @@ module DocumentationFormatter = sprintf "active pattern %s: %s" apc.Name findVal - let getAttributeSignature displayContext (attr: FSharpAttribute) = + let getAttributeSignature (attr: FSharpAttribute) = let name = formatShowDocumentationLink attr.AttributeType.DisplayName @@ -714,8 +717,7 @@ module DocumentationFormatter = |> Seq.map (fun inf -> fst (format displayContext inf)) |> Seq.toArray - let attrs = - fse.Attributes |> Seq.map (getAttributeSignature displayContext) |> Seq.toArray + let attrs = fse.Attributes |> Seq.map getAttributeSignature |> Seq.toArray let types = fse.NestedEntities @@ -782,7 +784,10 @@ module DocumentationFormatter = /// trims the leading 'Microsoft.' from the full name of the symbol member m.SafeFullName = - if m.FullName.StartsWith "Microsoft." && m.Assembly.SimpleName = "FSharp.Core" then + if + m.FullName.StartsWith("Microsoft.", StringComparison.Ordinal) + && m.Assembly.SimpleName = "FSharp.Core" + then m.FullName.Substring "Microsoft.".Length else m.FullName diff --git a/src/FsAutoComplete.Core/DotnetNewTemplate.fs b/src/FsAutoComplete.Core/DotnetNewTemplate.fs index 8fcdeb91c..d792b0c92 100644 --- a/src/FsAutoComplete.Core/DotnetNewTemplate.fs +++ b/src/FsAutoComplete.Core/DotnetNewTemplate.fs @@ -61,12 +61,12 @@ module DotnetNewTemplate = let parseTemplateOutput (x: string) = let xs = x.Split(Environment.NewLine) - |> Array.skipWhile (fun n -> not (n.StartsWith "Template")) + |> Array.skipWhile (fun n -> not (n.StartsWith("Template", StringComparison.Ordinal))) |> Array.filter (fun n -> not (n.StartsWith ' ' || String.IsNullOrWhiteSpace n)) let header = xs.[0] let body = xs.[2..] - let nameLength = header.IndexOf("Short") + let nameLength = header.IndexOf("Short", StringComparison.Ordinal) let body = body @@ -130,8 +130,11 @@ module DotnetNewTemplate = let templates = templateDetails () |> List.map (fun t -> t, extractDetailedString t) - |> List.filter (fun (t, strings) -> strings |> List.exists (nameMatch userInput)) - |> List.map (fun (t, strings) -> t) + |> List.choose (fun (t, strings) -> + if strings |> List.exists (nameMatch userInput) then + Some t + else + None) match templates with | [] -> failwithf "No template exists with name : %s" userInput diff --git a/src/FsAutoComplete.Core/FCSPatches.fs b/src/FsAutoComplete.Core/FCSPatches.fs index 2e8e559c5..2398f9252 100644 --- a/src/FsAutoComplete.Core/FCSPatches.fs +++ b/src/FsAutoComplete.Core/FCSPatches.fs @@ -478,7 +478,7 @@ module SyntaxTreeOps = (match copyInfo with | Some(e, _) -> walkExpr e | None -> false) - || walkExprs (recordFields |> List.map (fun (ident, range, expr) -> expr)) + || walkExprs (recordFields |> List.map (fun (_ident, _range, expr) -> expr)) | SynExpr.Record(copyInfo = copyInfo; recordFields = recordFields) -> (match copyInfo with @@ -542,11 +542,11 @@ module SyntaxTreeOps = | SynInterpolatedStringPart.String _ -> None | SynInterpolatedStringPart.FillExpr(x, _) -> Some x) ) - | SynExpr.IndexRange(expr1, opm, expr2, range1, range2, range3) -> + | SynExpr.IndexRange(expr1 = expr1; expr2 = expr2) -> Option.map walkExpr expr1 |> Option.orElseWith (fun _ -> Option.map walkExpr expr2) |> Option.defaultValue false - | SynExpr.IndexFromEnd(expr, range) -> walkExpr expr + | SynExpr.IndexFromEnd(expr, _) -> walkExpr expr | SynExpr.DebugPoint(innerExpr = expr) -> walkExpr expr | SynExpr.Dynamic(funcExpr = funcExpr; argExpr = argExpr) -> walkExpr funcExpr || walkExpr argExpr @@ -659,7 +659,7 @@ module LanguageVersionShim = /// A LanguageVersionShim from the parsed "--langversion:" or defaultLanguageVersion let fromFSharpProjectOptions (fpo: FSharpProjectOptions) = fpo.OtherOptions - |> Array.tryFind (fun x -> x.StartsWith("--langversion:")) + |> Array.tryFind (fun x -> x.StartsWith("--langversion:", StringComparison.Ordinal)) |> Option.map (fun x -> x.Split(":")[1]) |> Option.map (fun x -> LanguageVersionShim(x)) |> Option.defaultWith (fun () -> defaultLanguageVersion.Value) diff --git a/src/FsAutoComplete.Core/FileSystem.fs b/src/FsAutoComplete.Core/FileSystem.fs index 6fc1414a3..c7afa3af9 100644 --- a/src/FsAutoComplete.Core/FileSystem.fs +++ b/src/FsAutoComplete.Core/FileSystem.fs @@ -594,7 +594,7 @@ module Tokenizer = ) : Range voption = match text[range] with | Error _ -> ValueNone - | Ok rangeText when rangeText.EndsWith "``" -> + | Ok rangeText when rangeText.EndsWith("``", StringComparison.Ordinal) -> // find matching opening backticks // backticks cannot contain linebreaks -- even for Active Pattern: diff --git a/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj b/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj index 81df0fe33..0c8f3994a 100644 --- a/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj +++ b/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj @@ -50,6 +50,7 @@ + diff --git a/src/FsAutoComplete.Core/Fsdn.fs b/src/FsAutoComplete.Core/Fsdn.fs index 30108d1cf..5aa8fb2e2 100644 --- a/src/FsAutoComplete.Core/Fsdn.fs +++ b/src/FsAutoComplete.Core/Fsdn.fs @@ -67,7 +67,7 @@ let query (queryStr: string) = let info2 = v.api.name //return a list of strings - let infoNamespace = info2.``namespace`` + let _infoNamespace = info2.``namespace`` let infoClass = info2.class_name let infoMethod = info2.id diff --git a/src/FsAutoComplete.Core/InlayHints.fs b/src/FsAutoComplete.Core/InlayHints.fs index a46756f6f..18dda8872 100644 --- a/src/FsAutoComplete.Core/InlayHints.fs +++ b/src/FsAutoComplete.Core/InlayHints.fs @@ -100,7 +100,7 @@ type private FSharp.Compiler.CodeAnalysis.FSharpParseFileResults with match binding with | SynBinding( headPat = SynPat.Named(range = patRange) - returnInfo = Some(SynBindingReturnInfo(typeName = SynType.LongIdent(idents)))) -> Some patRange + returnInfo = Some(SynBindingReturnInfo(typeName = SynType.LongIdent _))) -> Some patRange | _ -> defaultTraverse binding } let result = SyntaxTraversal.Traverse(pos, x.ParseTree, visitor) @@ -148,7 +148,11 @@ module private ShouldCreate = [] - let private (|StartsWith|_|) (v: string) (fullName: string) = if fullName.StartsWith v then ValueSome() else ValueNone + let private (|StartsWith|_|) (v: string) (fullName: string) = + if fullName.StartsWith(v, StringComparison.Ordinal) then + ValueSome() + else + ValueNone // doesn't differentiate between modules, types, namespaces // -> is just for documentation in code [] @@ -206,7 +210,8 @@ module private ShouldCreate = let inline private isMeaningfulName (p: FSharpParameter) = p.DisplayName.Length > 2 - let inline private isOperator (func: FSharpMemberOrFunctionOrValue) = func.CompiledName.StartsWith "op_" + let inline private isOperator (func: FSharpMemberOrFunctionOrValue) = + func.CompiledName.StartsWith("op_", StringComparison.Ordinal) /// Doesn't consider lower/upper cases: /// * `areSame "foo" "FOO" = true` @@ -303,7 +308,7 @@ module private ShouldCreate = let inline private doesNotMatchArgumentText (parameterName: string) (userArgumentText: string) = parameterName <> userArgumentText - && not (userArgumentText.StartsWith parameterName) + && not (userArgumentText.StartsWith(parameterName, StringComparison.Ordinal)) let private isParamNamePostfixOfFuncName (func: FSharpMemberOrFunctionOrValue) (paramName: string) = let funcName = func.DisplayName.AsSpan() @@ -311,12 +316,12 @@ module private ShouldCreate = isPostfixOf funcName paramName - /// + /// /// We filter out parameters that generate lots of noise in hints. /// * parameter has no name /// * parameter has length > 2 /// * parameter is one of a set of 'known' names that clutter (like printfn formats) - /// * param & function is "well known"/commonly used + /// * param & function is "well known"/commonly used /// * parameter does match or is a pre/postfix of user-entered text /// * user-entered text does match or is a pre/postfix of parameter /// * parameter is postfix of function name @@ -374,7 +379,7 @@ type MissingExplicitType with if x.SpecialRules |> List.contains RemoveOptionFromType then // Optional parameter: // `static member F(?a) =` -> `: int`, NOT `: int option` - if typeName.EndsWith " option" then + if typeName.EndsWith(" option", StringComparison.Ordinal) then typeName.Substring(0, typeName.Length - " option".Length) else typeName @@ -615,7 +620,7 @@ let tryGetExplicitTypeInfo (text: IFSACSourceText, ast: ParsedInput) (pos: Posit | _ -> defaultTraverse expr member visitor.VisitPat(path, defaultTraverse, pat) = - let invalidPositionForTypeAnnotation (pos: Position) (path: SyntaxNode list) = + let invalidPositionForTypeAnnotation (path: SyntaxNode list) = match path with | SyntaxNode.SynExpr(SynExpr.LetOrUseBang(isUse = true)) :: _ -> // use! value = @@ -636,7 +641,7 @@ let tryGetExplicitTypeInfo (text: IFSACSourceText, ast: ParsedInput) (pos: Posit // see dotnet/fsharp#13115 // | _ when not (rangeContainsPos pat.Range pos) -> None | SynPat.Named(ident = SynIdent(ident = ident)) when - rangeContainsPos ident.idRange pos && invalidPositionForTypeAnnotation pos path + rangeContainsPos ident.idRange pos && invalidPositionForTypeAnnotation path -> ExplicitType.Invalid |> Some | SynPat.Named(ident = SynIdent(ident = ident); isThisVal = false) when rangeContainsPos ident.idRange pos -> @@ -765,7 +770,7 @@ let private getArgRangesOfFunctionApplication (ast: ParsedInput) pos = { new SyntaxVisitorBase<_>() with member _.VisitExpr(_, traverseSynExpr, defaultTraverse, expr) = match expr with - | SynExpr.App(isInfix = false; funcExpr = funcExpr; argExpr = argExpr; range = range) when pos = range.Start -> + | SynExpr.App(isInfix = false; funcExpr = funcExpr; range = range) when pos = range.Start -> let isInfixFuncExpr = match funcExpr with | SynExpr.App(_, isInfix, _, _, _) -> isInfix @@ -823,7 +828,7 @@ let private getArgRangesOfFunctionApplication (ast: ParsedInput) pos = /// `let map f v = f v` -> `f` is target let isPotentialTargetForTypeAnnotation (allowFunctionValues: bool) - (symbolUse: FSharpSymbolUse, mfv: FSharpMemberOrFunctionOrValue) + (_symbolUse: FSharpSymbolUse, mfv: FSharpMemberOrFunctionOrValue) = //ENHANCEMENT: extract settings (mfv.IsValue || (allowFunctionValues && mfv.IsFunction)) diff --git a/src/FsAutoComplete.Core/Lexer.fs b/src/FsAutoComplete.Core/Lexer.fs index 8767d6a88..3168352b7 100644 --- a/src/FsAutoComplete.Core/Lexer.fs +++ b/src/FsAutoComplete.Core/Lexer.fs @@ -1,5 +1,6 @@ namespace FsAutoComplete +open System open FSharp.Compiler.Tokenization open FsAutoComplete.Logging open FsAutoComplete.Logging.Types @@ -43,14 +44,14 @@ module Lexer = [] let (|Define|_|) (a: string) = - if a.StartsWith "--define:" then + if a.StartsWith("--define:", StringComparison.Ordinal) then ValueSome(a.[9..]) else ValueNone [] let (|LangVersion|_|) (a: string) = - if a.StartsWith "--langversion:" then + if a.StartsWith("--langversion:", StringComparison.Ordinal) then ValueSome(a.[14..]) else ValueNone diff --git a/src/FsAutoComplete.Core/ParseAndCheckResults.fs b/src/FsAutoComplete.Core/ParseAndCheckResults.fs index 59d9d610a..d912e4e19 100644 --- a/src/FsAutoComplete.Core/ParseAndCheckResults.fs +++ b/src/FsAutoComplete.Core/ParseAndCheckResults.fs @@ -111,12 +111,12 @@ type ParseAndCheckResults (sym: FindDeclExternalSymbol) : (string * Position) option = match sym with - | FindDeclExternalSymbol.Type name -> None - | FindDeclExternalSymbol.Constructor(typeName, args) -> None - | FindDeclExternalSymbol.Method(typeName, name, paramSyms, genericArity) -> None - | FindDeclExternalSymbol.Field(typeName, name) -> None - | FindDeclExternalSymbol.Event(typeName, name) -> None - | FindDeclExternalSymbol.Property(typeName, name) -> None + | FindDeclExternalSymbol.Type _ -> None + | FindDeclExternalSymbol.Constructor _ -> None + | FindDeclExternalSymbol.Method _ -> None + | FindDeclExternalSymbol.Field _ -> None + | FindDeclExternalSymbol.Event _ -> None + | FindDeclExternalSymbol.Property _ -> None // attempts to manually discover symbol use and external symbol information for a range that doesn't exist in a local file // bugfix/workaround for FCS returning invalid decl found for f# members. @@ -163,7 +163,9 @@ type ParseAndCheckResults | FindDeclFailureReason.Unknown r -> r return ResultOrString.Error(sprintf "Could not find declaration. %s" elaboration) - | FindDeclResult.DeclFound range when range.FileName.EndsWith(Range.rangeStartup.FileName) -> + | FindDeclResult.DeclFound range when + range.FileName.EndsWith(Range.rangeStartup.FileName, StringComparison.Ordinal) + -> return ResultOrString.Error "Could not find declaration" | FindDeclResult.DeclFound range when range.FileName = UMX.untag x.FileName -> // decl in same file @@ -215,7 +217,7 @@ type ParseAndCheckResults { File = UMX.untag localFilePath Position = pos } ) - | Error reason -> + | Error _ -> logger.info ( Log.setMessage "no sourcelink info for {assembly}, decompiling instead" >> Log.addContextDestructured "assembly" assembly @@ -318,7 +320,9 @@ type ParseAndCheckResults member __.TryGetToolTip (pos: Position) (lineStr: LineStr) = match Lexer.findLongIdents (pos.Column, lineStr) with - | None -> ResultOrString.Error "Cannot find ident for tooltip" + | None -> + logger.info (Log.setMessageI $"Cannot find ident for tooltip: {pos.Column:column} in {lineStr:lineString}") + None | Some(col, identIsland) -> let identIsland = Array.toList identIsland // TODO: Display other tooltip types, for example for strings or comments where appropriate @@ -330,15 +334,16 @@ type ParseAndCheckResults match identIsland with | [ ident ] -> match KeywordList.keywordTooltips.TryGetValue ident with - | true, tip -> Ok tip - | _ -> ResultOrString.Error "No tooltip information" - | _ -> ResultOrString.Error "No tooltip information" - | _ -> Ok(tip) - - member x.TryGetToolTipEnhanced - (pos: Position) - (lineStr: LineStr) - : Result, string> = + | true, tip -> Some tip + | _ -> + logger.info (Log.setMessageI $"Cannot find ident for tooltip: {pos.Column:column} in {lineStr:lineString}") + None + | _ -> + logger.info (Log.setMessageI $"Cannot find ident for tooltip: {pos.Column:column} in {lineStr:lineString}") + None + | _ -> Some tip + + member x.TryGetToolTipEnhanced (pos: Position) (lineStr: LineStr) : option = let (|EmptyTooltip|_|) (ToolTipText elems) = match elems with | [] -> Some() @@ -346,11 +351,13 @@ type ParseAndCheckResults | _ -> None match Completion.atPos (pos, x.GetParseResults.ParseTree) with - | Completion.Context.StringLiteral -> Ok None + | Completion.Context.StringLiteral -> None | Completion.Context.SynType | Completion.Context.Unknown -> match Lexer.findLongIdents (pos.Column, lineStr) with - | None -> Error "Cannot find ident for tooltip" + | None -> + logger.info (Log.setMessageI $"Cannot find ident for tooltip: {pos.Column:column} in {lineStr:lineString}") + None | Some(col, identIsland) -> let identIsland = Array.toList identIsland // TODO: Display other tooltip types, for example for strings or comments where appropriate @@ -371,12 +378,20 @@ type ParseAndCheckResults Footer = "" SymbolInfo = TryGetToolTipEnhancedResult.Keyword ident } |> Some - |> Ok - | _ -> Error "No tooltip information" - | _ -> Error "No tooltip information" + | _ -> + logger.info ( + Log.setMessageI $"Cannot find ident for tooltip: {pos.Column:column} in {lineStr:lineString}" + ) + + None + | _ -> + logger.info (Log.setMessageI $"Cannot find ident for tooltip: {pos.Column:column} in {lineStr:lineString}") + None | _ -> match symbol with - | None -> Error "No tooltip information" + | None -> + logger.info (Log.setMessageI $"Cannot find ident for tooltip: {pos.Column:column} in {lineStr:lineString}") + None | Some symbol -> // Retrieve the FSharpSymbol instance so we can find the XmlDocSig @@ -386,7 +401,12 @@ type ParseAndCheckResults let resolvedType = symbol.Symbol.GetAbbreviatedParent() match SignatureFormatter.getTooltipDetailsFromSymbolUse symbol with - | None -> Error "No tooltip information" + | None -> + logger.info ( + Log.setMessageI $"Cannot find tooltip for {symbol:symbol} ({pos.Column:column} in {lineStr:lineString})" + ) + + None | Some(signature, footer) -> { ToolTipText = tip Signature = signature @@ -396,7 +416,6 @@ type ParseAndCheckResults {| XmlDocSig = resolvedType.XmlDocSig Assembly = symbol.Symbol.Assembly.SimpleName |} } |> Some - |> Ok member __.TryGetFormattedDocumentation (pos: Position) (lineStr: LineStr) = match Lexer.findLongIdents (pos.Column, lineStr) with diff --git a/src/FsAutoComplete.Core/ParseAndCheckResults.fsi b/src/FsAutoComplete.Core/ParseAndCheckResults.fsi index 73ae4f0dc..f3099f45a 100644 --- a/src/FsAutoComplete.Core/ParseAndCheckResults.fsi +++ b/src/FsAutoComplete.Core/ParseAndCheckResults.fsi @@ -47,9 +47,9 @@ type ParseAndCheckResults = member TryFindIdentifierDeclaration: pos: Position -> lineStr: LineStr -> Async> member TryFindTypeDeclaration: pos: Position -> lineStr: LineStr -> Async> - member TryGetToolTip: pos: Position -> lineStr: LineStr -> Result + member TryGetToolTip: pos: Position -> lineStr: LineStr -> ToolTipText option - member TryGetToolTipEnhanced: pos: Position -> lineStr: LineStr -> Result + member TryGetToolTipEnhanced: pos: Position -> lineStr: LineStr -> TryGetToolTipEnhancedResult option member TryGetFormattedDocumentation: pos: Position -> diff --git a/src/FsAutoComplete.Core/SignatureFormatter.fs b/src/FsAutoComplete.Core/SignatureFormatter.fs index 6892610ca..476321d2e 100644 --- a/src/FsAutoComplete.Core/SignatureFormatter.fs +++ b/src/FsAutoComplete.Core/SignatureFormatter.fs @@ -167,7 +167,7 @@ module SignatureFormatter = let typeList = unionCase.Fields |> Seq.map (fun unionField -> - if unionField.Name.StartsWith "Item" then //TODO: Some better way of detecting default names for the union cases' fields + if unionField.Name.StartsWith("Item", StringComparison.Ordinal) then //TODO: Some better way of detecting default names for the union cases' fields formatFSharpType displayContext unionField.FieldType else unionField.Name + ":" ++ (formatFSharpType displayContext unionField.FieldType)) @@ -178,7 +178,7 @@ module SignatureFormatter = unionCase.DisplayName let getFuncSignatureWithIdent displayContext (func: FSharpMemberOrFunctionOrValue) (ident: int) = - let maybeGetter = func.LogicalName.StartsWith "get_" + let maybeGetter = func.LogicalName.StartsWith("get_", StringComparison.Ordinal) let indent = String.replicate ident " " let functionName = @@ -190,7 +190,7 @@ module SignatureFormatter = |> FSharpKeywords.NormalizeIdentifierBackticks elif func.IsOperatorOrActivePattern then $"( {func.DisplayNameCore} )" - elif func.DisplayName.StartsWith "( " then + elif func.DisplayName.StartsWith("( ", StringComparison.Ordinal) then FSharpKeywords.NormalizeIdentifierBackticks func.LogicalName else FSharpKeywords.NormalizeIdentifierBackticks func.DisplayName @@ -377,9 +377,12 @@ module SignatureFormatter = "new" elif func.IsOperatorOrActivePattern then func.DisplayName - elif func.DisplayName.StartsWith "( " then + elif func.DisplayName.StartsWith("( ", StringComparison.Ordinal) then FSharpKeywords.NormalizeIdentifierBackticks func.LogicalName - elif func.LogicalName.StartsWith "get_" || func.LogicalName.StartsWith "set_" then + elif + func.LogicalName.StartsWith("get_", StringComparison.Ordinal) + || func.LogicalName.StartsWith("set_", StringComparison.Ordinal) + then PrettyNaming.TryChopPropertyName func.DisplayName |> Option.defaultValue func.DisplayName else @@ -515,7 +518,7 @@ module SignatureFormatter = let prefix = if v.IsMutable then "val mutable" else "val" let name = - (if v.DisplayName.StartsWith "( " then + (if v.DisplayName.StartsWith("( ", StringComparison.Ordinal) then v.LogicalName else v.DisplayName) @@ -549,10 +552,12 @@ module SignatureFormatter = let getAPCaseSignature displayContext (apc: FSharpActivePatternCase) = let findVal = + let apcSearchString = $"|{apc.DisplayName}|" + apc.Group.DeclaringEntity |> Option.bind (fun ent -> ent.MembersFunctionsAndValues - |> Seq.tryFind (fun func -> func.DisplayName.Contains apc.DisplayName) + |> Seq.tryFind (fun func -> func.DisplayName.Contains(apcSearchString, StringComparison.OrdinalIgnoreCase)) |> Option.map (getFuncSignature displayContext)) |> Option.bind (fun n -> try diff --git a/src/FsAutoComplete.Core/SignatureHelp.fs b/src/FsAutoComplete.Core/SignatureHelp.fs index 95dca906d..e3f059674 100644 --- a/src/FsAutoComplete.Core/SignatureHelp.fs +++ b/src/FsAutoComplete.Core/SignatureHelp.fs @@ -141,7 +141,11 @@ let private getSignatureHelpForMethod let methods = methodGroup.Methods - do! Option.guard (methods.Length > 0 && not (methodGroup.MethodName.EndsWith("> )"))) + do! + Option.guard ( + methods.Length > 0 + && not (methodGroup.MethodName.EndsWith("> )", StringComparison.Ordinal)) + ) let isStaticArgTip = lines.TryGetChar paramLocations.OpenParenLocation = Some '<' diff --git a/src/FsAutoComplete.Core/State.fs b/src/FsAutoComplete.Core/State.fs index 9c49dbb28..dafcc3e8d 100644 --- a/src/FsAutoComplete.Core/State.fs +++ b/src/FsAutoComplete.Core/State.fs @@ -16,41 +16,6 @@ open FCSPatches [] module ProjInfoExtensions = - let private internalGetCSharpReferenceInfo = - fun (r: FSharpReferencedProject) -> - let rCase, fields = - FSharp.Reflection.FSharpValue.GetUnionFields( - r, - typeof, - System.Reflection.BindingFlags.Public - ||| System.Reflection.BindingFlags.NonPublic - ||| System.Reflection.BindingFlags.Instance - ) - - if rCase.Name = "PEReference" then - let getStamp: unit -> DateTime = fields[0] :?> _ - let reader = fields[1] - Some(getStamp, reader) - else - None - - let private internalGetProjectOptions = - fun (r: FSharpReferencedProject) -> - let rCase, fields = - FSharp.Reflection.FSharpValue.GetUnionFields( - r, - typeof, - System.Reflection.BindingFlags.Public - ||| System.Reflection.BindingFlags.NonPublic - ||| System.Reflection.BindingFlags.Instance - ) - - if rCase.Name = "FSharpReference" then - let projOptions: FSharpProjectOptions = rCase.GetFields().[1].GetValue(box r) :?> _ - Some projOptions - else - None - type FSharpReferencedProject with member x.ProjectFilePath = @@ -71,7 +36,9 @@ module ProjInfoExtensions = type FSharpProjectOptions with member x.OutputDll = - x.OtherOptions |> Array.find (fun o -> o.StartsWith("-o:")) |> (fun s -> s[3..]) + x.OtherOptions + |> Array.find (fun o -> o.StartsWith("-o:", StringComparison.Ordinal)) + |> (fun s -> s[3..]) member x.SourceFilesThatThisFileDependsOn(file: string) = let untagged = UMX.untag file @@ -88,7 +55,7 @@ module ProjInfoExtensions = match Array.tryFindIndex ((=) untagged) x.SourceFiles with | None -> [||] | Some index when index < x.SourceFiles.Length -> x.SourceFiles[index + 1 ..] - | Some index -> [||] // at the end, so return empty list + | Some _ -> [||] // at the end, so return empty list type ProjectController with diff --git a/src/FsAutoComplete.Core/TestAdapter.fs b/src/FsAutoComplete.Core/TestAdapter.fs index fdd944567..fe75b9a65 100644 --- a/src/FsAutoComplete.Core/TestAdapter.fs +++ b/src/FsAutoComplete.Core/TestAdapter.fs @@ -1,6 +1,6 @@ module FsAutoComplete.TestAdapter - +open System open FSharp.Compiler.Text open FSharp.Compiler.Syntax @@ -44,46 +44,50 @@ let getExpectoTests (ast: ParsedInput) : TestAdapterEntry list = let mutable ident = 0 let isExpectoName (str: string) = - str.EndsWith "testCase" - || str.EndsWith "ftestCase" - || str.EndsWith "ptestCase" - || str.EndsWith "testCaseAsync" - || str.EndsWith "ftestCaseAsync" - || str.EndsWith "ptestCaseAsync" - || str.EndsWith "testCaseTask" - || str.EndsWith "ftestCaseTask" - || str.EndsWith "ptestCaseTask" - || (str.EndsWith "test" - && not (str.EndsWith "failtest") - && not (str.EndsWith "skiptest")) - || str.EndsWith "ftest" - || (str.EndsWith "ptest" && not (str.EndsWith "skiptest")) - || str.EndsWith "testAsync" - || str.EndsWith "ftestAsync" - || str.EndsWith "ptestAsync" - || str.EndsWith "testTask" - || str.EndsWith "ftestTask" - || str.EndsWith "ptestTask" - || str.EndsWith "testProperty" - || str.EndsWith "ptestProperty" - || str.EndsWith "ftestProperty" - || str.EndsWith "testPropertyWithConfig" - || str.EndsWith "ptestPropertyWithConfig" - || str.EndsWith "ftestPropertyWithConfig" - || str.EndsWith "testPropertyWithConfigs" - || str.EndsWith "ptestPropertyWithConfigs" - || str.EndsWith "ftestPropertyWithConfigs" - || str.EndsWith "testTheory" - || str.EndsWith "ftestTheory" - || str.EndsWith "ptestTheory" - || str.EndsWith "testTheoryAsync" - || str.EndsWith "ftestTheoryAsync" - || str.EndsWith "ptestTheoryAsync" - || str.EndsWith "testTheoryTask" - || str.EndsWith "ftestTheoryTask" - || str.EndsWith "ptestTheoryTask" - - let isExpectoListName (str: string) = str.EndsWith "testList" || str.EndsWith "ftestList" || str.EndsWith "ptestList" + str.EndsWith("testCase", StringComparison.Ordinal) + || str.EndsWith("ftestCase", StringComparison.Ordinal) + || str.EndsWith("ptestCase", StringComparison.Ordinal) + || str.EndsWith("testCaseAsync", StringComparison.Ordinal) + || str.EndsWith("ftestCaseAsync", StringComparison.Ordinal) + || str.EndsWith("ptestCaseAsync", StringComparison.Ordinal) + || str.EndsWith("testCaseTask", StringComparison.Ordinal) + || str.EndsWith("ftestCaseTask", StringComparison.Ordinal) + || str.EndsWith("ptestCaseTask", StringComparison.Ordinal) + || (str.EndsWith("test", StringComparison.Ordinal) + && not (str.EndsWith("failtest", StringComparison.Ordinal)) + && not (str.EndsWith("skiptest", StringComparison.Ordinal))) + || str.EndsWith("ftest", StringComparison.Ordinal) + || (str.EndsWith("ptest", StringComparison.Ordinal) + && not (str.EndsWith("skiptest", StringComparison.Ordinal))) + || str.EndsWith("testAsync", StringComparison.Ordinal) + || str.EndsWith("ftestAsync", StringComparison.Ordinal) + || str.EndsWith("ptestAsync", StringComparison.Ordinal) + || str.EndsWith("testTask", StringComparison.Ordinal) + || str.EndsWith("ftestTask", StringComparison.Ordinal) + || str.EndsWith("ptestTask", StringComparison.Ordinal) + || str.EndsWith("testProperty", StringComparison.Ordinal) + || str.EndsWith("ptestProperty", StringComparison.Ordinal) + || str.EndsWith("ftestProperty", StringComparison.Ordinal) + || str.EndsWith("testPropertyWithConfig", StringComparison.Ordinal) + || str.EndsWith("ptestPropertyWithConfig", StringComparison.Ordinal) + || str.EndsWith("ftestPropertyWithConfig", StringComparison.Ordinal) + || str.EndsWith("testPropertyWithConfigs", StringComparison.Ordinal) + || str.EndsWith("ptestPropertyWithConfigs", StringComparison.Ordinal) + || str.EndsWith("ftestPropertyWithConfigs", StringComparison.Ordinal) + || str.EndsWith("testTheory", StringComparison.Ordinal) + || str.EndsWith("ftestTheory", StringComparison.Ordinal) + || str.EndsWith("ptestTheory", StringComparison.Ordinal) + || str.EndsWith("testTheoryAsync", StringComparison.Ordinal) + || str.EndsWith("ftestTheoryAsync", StringComparison.Ordinal) + || str.EndsWith("ptestTheoryAsync", StringComparison.Ordinal) + || str.EndsWith("testTheoryTask", StringComparison.Ordinal) + || str.EndsWith("ftestTheoryTask", StringComparison.Ordinal) + || str.EndsWith("ptestTheoryTask", StringComparison.Ordinal) + + let isExpectoListName (str: string) = + str.EndsWith("testList", StringComparison.Ordinal) + || str.EndsWith("ftestList", StringComparison.Ordinal) + || str.EndsWith("ptestList", StringComparison.Ordinal) let (|Case|List|NotExpecto|) = function @@ -266,16 +270,16 @@ let getNUnitTest (ast: ParsedInput) : TestAdapterEntry list = |> List.exists (fun a -> let str = a.TypeName.LongIdent |> List.last - str.idText.EndsWith "Test" - || str.idText.EndsWith "TestAttribute" - || str.idText.EndsWith "TestCase" - || str.idText.EndsWith "TestCaseAttribute" - || str.idText.EndsWith "TestCaseSource" - || str.idText.EndsWith "TestCaseSourceAttribute" - || str.idText.EndsWith "Theory" - || str.idText.EndsWith "TheoryAttribute" - || str.idText.EndsWith "Property" - || str.idText.EndsWith "PropertyAttribute") + str.idText.EndsWith("Test", StringComparison.Ordinal) + || str.idText.EndsWith("TestAttribute", StringComparison.Ordinal) + || str.idText.EndsWith("TestCase", StringComparison.Ordinal) + || str.idText.EndsWith("TestCaseAttribute", StringComparison.Ordinal) + || str.idText.EndsWith("TestCaseSource", StringComparison.Ordinal) + || str.idText.EndsWith("TestCaseSourceAttribute", StringComparison.Ordinal) + || str.idText.EndsWith("Theory", StringComparison.Ordinal) + || str.idText.EndsWith("TheoryAttribute", StringComparison.Ordinal) + || str.idText.EndsWith("Property", StringComparison.Ordinal) + || str.idText.EndsWith("PropertyAttribute", StringComparison.Ordinal)) let getName = function @@ -444,12 +448,12 @@ let getXUnitTest ast : TestAdapterEntry list = |> List.exists (fun a -> let str = a.TypeName.LongIdent |> List.last - str.idText.EndsWith "Fact" - || str.idText.EndsWith "FactAttribute" - || str.idText.EndsWith "Theory" - || str.idText.EndsWith "TheoryAttribute" - || str.idText.EndsWith "Property" - || str.idText.EndsWith "PropertyAttribute") + str.idText.EndsWith("Fact", StringComparison.Ordinal) + || str.idText.EndsWith("FactAttribute", StringComparison.Ordinal) + || str.idText.EndsWith("Theory", StringComparison.Ordinal) + || str.idText.EndsWith("TheoryAttribute", StringComparison.Ordinal) + || str.idText.EndsWith("Property", StringComparison.Ordinal) + || str.idText.EndsWith("PropertyAttribute", StringComparison.Ordinal)) let getName = function diff --git a/src/FsAutoComplete.Core/TipFormatter.fs b/src/FsAutoComplete.Core/TipFormatter.fs index 296440bcd..dcc68102a 100644 --- a/src/FsAutoComplete.Core/TipFormatter.fs +++ b/src/FsAutoComplete.Core/TipFormatter.fs @@ -177,7 +177,9 @@ module private Format = // so render the markdown code block the best way // by avoid empty lines at the beginning or the end let formattedText = - match innerText.StartsWith("\n"), innerText.EndsWith("\n") with + match + innerText.StartsWith("\n", StringComparison.Ordinal), innerText.EndsWith("\n", StringComparison.Ordinal) + with | true, true -> sprintf "```%s%s```" lang innerText | true, false -> sprintf "```%s%s\n```" lang innerText | false, true -> sprintf "```%s\n%s```" lang innerText @@ -733,7 +735,10 @@ type private XmlDocMember(doc: XmlDocument, indentationSize: int, columnOffset: content.Split('\n') |> Array.map (fun line -> - if not (String.IsNullOrWhiteSpace line) && line.StartsWith(tabsOffset) then + if + not (String.IsNullOrWhiteSpace line) + && line.StartsWith(tabsOffset, StringComparison.Ordinal) + then line.Substring(columnOffset + indentationSize) else line) @@ -890,6 +895,26 @@ let rec private readXmlDoc (reader: XmlReader) (indentationSize: int) (acc: Map< let private xmlDocCache = Collections.Concurrent.ConcurrentDictionary>() +let private findCultures v = + let rec loop state (v: System.Globalization.CultureInfo) = + let state' = v.Name :: state + + if v.Parent = System.Globalization.CultureInfo.InvariantCulture then + "" :: state' |> List.rev + else + loop state' v.Parent + + loop [] v + +let private findLocalizedXmlFile (xmlFile: string) = + let xmlName = Path.GetFileName xmlFile + let path = Path.GetDirectoryName xmlFile + + findCultures System.Globalization.CultureInfo.CurrentUICulture + |> List.map (fun culture -> Path.Combine(path, culture, xmlName)) + |> List.tryFind (fun i -> File.Exists i) + |> Option.defaultValue xmlFile + let private getXmlDoc dllFile = let xmlFile = Path.ChangeExtension(dllFile, ".xml") //Workaround for netstandard.dll @@ -903,6 +928,8 @@ let private getXmlDoc dllFile = else xmlFile + let xmlFile = findLocalizedXmlFile xmlFile + if xmlDocCache.ContainsKey xmlFile then Some xmlDocCache.[xmlFile] else @@ -973,7 +1000,7 @@ let private tryGetXmlDocMember (xmlDoc: FSharpXmlDoc) = let rec findIndentationSize (lines: string list) = match lines with | head :: tail -> - let lesserThanIndex = head.IndexOf("<") + let lesserThanIndex = head.IndexOf("<", StringComparison.Ordinal) if lesserThanIndex <> -1 then lesserThanIndex diff --git a/src/FsAutoComplete.Core/TypedAstPatterns.fs b/src/FsAutoComplete.Core/TypedAstPatterns.fs index 2959e1921..55c765a87 100644 --- a/src/FsAutoComplete.Core/TypedAstPatterns.fs +++ b/src/FsAutoComplete.Core/TypedAstPatterns.fs @@ -70,7 +70,7 @@ module SymbolUse = | :? FSharpParameter as param -> Some param | _ -> None - let (|StaticParameter|_|) (symbol: FSharpSymbolUse) = Some + let (|StaticParameter|_|) (_symbol: FSharpSymbolUse) = Some let (|UnionCase|_|) (symbol: FSharpSymbolUse) = @@ -411,7 +411,7 @@ module SymbolPatterns = else None - let (|ProvidedType|_|) (e: FSharpEntity) = None + let (|ProvidedType|_|) (_e: FSharpEntity) = None let (|ByRef|_|) (e: FSharpEntity) = if e.IsByRef then Some() else None @@ -421,7 +421,7 @@ module SymbolPatterns = let (|Namespace|_|) (entity: FSharpEntity) = if entity.IsNamespace then Some() else None - let (|ProvidedAndErasedType|_|) (entity: FSharpEntity) = None + let (|ProvidedAndErasedType|_|) (_entity: FSharpEntity) = None let (|Enum|_|) (entity: FSharpEntity) = if entity.IsEnum then Some() else None diff --git a/src/FsAutoComplete.Core/TypedAstPatterns.fsi b/src/FsAutoComplete.Core/TypedAstPatterns.fsi index 91ff92fc3..2cdc67b71 100644 --- a/src/FsAutoComplete.Core/TypedAstPatterns.fsi +++ b/src/FsAutoComplete.Core/TypedAstPatterns.fsi @@ -13,7 +13,7 @@ module SymbolUse = val (|MemberFunctionOrValue|_|): symbol: FSharpSymbolUse -> FSharpMemberOrFunctionOrValue option val (|ActivePattern|_|): (FSharpSymbolUse -> FSharpMemberOrFunctionOrValue option) val (|Parameter|_|): symbol: FSharpSymbolUse -> FSharpParameter option - val (|StaticParameter|_|): symbol: FSharpSymbolUse -> ('a -> 'a option) + val (|StaticParameter|_|): FSharpSymbolUse -> ('a -> 'a option) val (|UnionCase|_|): symbol: FSharpSymbolUse -> FSharpUnionCase option val (|Constructor|_|): (FSharpSymbolUse -> FSharpMemberOrFunctionOrValue option) val (|TypeAbbreviation|_|): (FSharpSymbolUse -> FSharpEntity option) @@ -56,12 +56,12 @@ module SymbolPatterns = val (|Interface|_|): e: FSharpEntity -> unit option val (|AbstractClass|_|): e: FSharpEntity -> unit option val (|FSharpType|_|): e: FSharpEntity -> unit option - val (|ProvidedType|_|): e: FSharpEntity -> 'a option + val (|ProvidedType|_|): FSharpEntity -> 'a option val (|ByRef|_|): e: FSharpEntity -> unit option val (|Array|_|): e: FSharpEntity -> unit option val (|FSharpModule|_|): entity: FSharpEntity -> unit option val (|Namespace|_|): entity: FSharpEntity -> unit option - val (|ProvidedAndErasedType|_|): entity: FSharpEntity -> 'a option + val (|ProvidedAndErasedType|_|): FSharpEntity -> 'a option val (|Enum|_|): entity: FSharpEntity -> unit option val (|Tuple|_|): ty: FSharpType option -> unit option val (|RefCell|_|): ty: FSharpType -> unit option diff --git a/src/FsAutoComplete.Core/TypedAstUtils.fs b/src/FsAutoComplete.Core/TypedAstUtils.fs index 86d417302..601167e30 100644 --- a/src/FsAutoComplete.Core/TypedAstUtils.fs +++ b/src/FsAutoComplete.Core/TypedAstUtils.fs @@ -57,8 +57,8 @@ module TypedAstUtils = |> Option.isSome let isOperator (name: string) = - name.StartsWith "( " - && name.EndsWith " )" + name.StartsWith("( ", StringComparison.Ordinal) + && name.EndsWith(" )", StringComparison.Ordinal) && name.Length > 4 && name.Substring(2, name.Length - 4) |> String.forall (fun c -> c <> ' ' && not (Char.IsLetter c)) @@ -190,10 +190,14 @@ module TypedAstExtensionHelpers = member x.IsConstructor = x.CompiledName = ".ctor" member x.IsOperatorOrActivePattern = - x.CompiledName.StartsWith "op_" + x.CompiledName.StartsWith("op_", StringComparison.Ordinal) || let name = x.DisplayName in - if name.StartsWith "( " && name.EndsWith " )" && name.Length > 4 then + if + name.StartsWith("( ", StringComparison.Ordinal) + && name.EndsWith(" )", StringComparison.Ordinal) + && name.Length > 4 + then name.Substring(2, name.Length - 4) |> String.forall (fun c -> c <> ' ') else false @@ -283,7 +287,7 @@ module TypedAstExtensionHelpers = | UnionCase fsu -> fsu.XmlDoc | ActivePattern apc -> apc.XmlDoc | GenericParameter gp -> gp.XmlDoc - | Parameter p -> FSharpXmlDoc.None + | Parameter _ -> FSharpXmlDoc.None type FSharpGenericParameterMemberConstraint with diff --git a/src/FsAutoComplete.Core/UnionPatternMatchCaseGenerator.fs b/src/FsAutoComplete.Core/UnionPatternMatchCaseGenerator.fs index 8dbc3963b..066f15385 100644 --- a/src/FsAutoComplete.Core/UnionPatternMatchCaseGenerator.fs +++ b/src/FsAutoComplete.Core/UnionPatternMatchCaseGenerator.fs @@ -2,10 +2,8 @@ module FsAutoComplete.UnionPatternMatchCaseGenerator open System -open FsAutoComplete.UntypedAstUtils open FSharp.Compiler.Syntax open FSharp.Compiler.Text -open FSharp.Compiler.Syntax open FsAutoComplete.CodeGenerationUtils open FSharp.Compiler.Symbols open FsToolkit.ErrorHandling @@ -40,11 +38,11 @@ let private clauseIsCandidateForCodeGen (cursorPos: Position) (SynMatchClause(pa match pat with | SynPat.Paren(innerPat, _) | SynPat.Attrib(innerPat, _, _) -> patIsCandidate innerPat - | SynPat.Const(_, _) -> false - | SynPat.Wild(_) -> false + | SynPat.Const _ -> false + | SynPat.Wild _ -> false // TODO: check if we have to handle these cases | SynPat.Typed(innerPat, _, _) -> patIsCandidate innerPat - | SynPat.OptionalVal(_, _) -> false + | SynPat.OptionalVal _ -> false | SynPat.Or(lhsPat = leftPat; rhsPat = rightPat) -> patIsCandidate leftPat || patIsCandidate rightPat | SynPat.Ands(innerPatList, _) -> List.exists patIsCandidate innerPatList // This is the 'hd :: tail -> ...' pattern @@ -53,7 +51,7 @@ let private clauseIsCandidateForCodeGen (cursorPos: Position) (SynMatchClause(pa // The cursor should not be in the nested patterns Range.rangeContainsPos r cursorPos && List.forall (not << patIsCandidate) nestedPats - | SynPat.ListCons(lhs, rhs, r, _) -> patIsCandidate lhs || patIsCandidate rhs + | SynPat.ListCons(lhs, rhs, _, _) -> patIsCandidate lhs || patIsCandidate rhs | SynPat.Tuple _ | SynPat.ArrayOrList _ | SynPat.Record _ @@ -122,7 +120,7 @@ let private tryFindPatternMatchExprInParsedInput (pos: Position) (parsedInput: P | SynMemberDefn.NestedType(typeDef, _access, _range) -> walkSynTypeDefn typeDef | SynMemberDefn.ValField(_field, _range) -> None | SynMemberDefn.LetBindings(bindings, _isStatic, _isRec, _range) -> List.tryPick walkBinding bindings - | SynMemberDefn.GetSetMember(_get, _set, _range, trivia) -> None + | SynMemberDefn.GetSetMember(_get, _set, _range, _) -> None | SynMemberDefn.Open _ | SynMemberDefn.ImplicitInherit _ | SynMemberDefn.Inherit _ @@ -242,7 +240,7 @@ let private tryFindPatternMatchExprInParsedInput (pos: Position) (parsedInput: P | SynExpr.DotSet(synExpr1, _longIdent, synExpr2, _range) -> List.tryPick walkExpr [ synExpr1; synExpr2 ] - | SynExpr.DotIndexedGet(synExpr, argList, _range, _range2) -> walkExpr argList + | SynExpr.DotIndexedGet(_, argList, _range, _range2) -> walkExpr argList | SynExpr.DotIndexedSet(synExpr1, argList, synExpr2, _, _range, _range2) -> [ synExpr1; argList; synExpr2 ] |> List.tryPick walkExpr @@ -297,16 +295,16 @@ let getWrittenCases (patMatchExpr: PatternMatchExpr) = match pat with | SynPat.Const(_const, _) -> false // TODO: figure out if these cases are supposed to happen or not - | SynPat.Or(_) - | SynPat.Ands(_, _) - | SynPat.LongIdent(_) - | SynPat.ArrayOrList(_, _, _) - | SynPat.Null(_) - | SynPat.InstanceMember(_, _, _, _, _) - | SynPat.IsInst(_, _) - | SynPat.QuoteExpr(_, _) + | SynPat.Or _ + | SynPat.Ands _ + | SynPat.LongIdent _ + | SynPat.ArrayOrList _ + | SynPat.Null _ + | SynPat.InstanceMember _ + | SynPat.IsInst _ + | SynPat.QuoteExpr _ | SynPat.ListCons _ - | SynPat.FromParseError(_, _) -> false + | SynPat.FromParseError _ -> false | SynPat.Tuple(elementPats = innerPatList) -> List.forall checkPattern innerPatList @@ -315,9 +313,9 @@ let getWrittenCases (patMatchExpr: PatternMatchExpr) = |> List.map (fun (_, _, innerPat) -> innerPat) |> List.forall checkPattern - | SynPat.OptionalVal(_, _) -> true - | SynPat.Named(_) - | SynPat.Wild(_) -> true + | SynPat.OptionalVal _ -> true + | SynPat.Named _ + | SynPat.Wild _ -> true | SynPat.Typed(innerPat, _, _) | SynPat.Attrib(innerPat, _, _) | SynPat.Paren(innerPat, _) -> checkPattern innerPat @@ -365,7 +363,7 @@ let getWrittenCases (patMatchExpr: PatternMatchExpr) = | SynMatchClause(pat, None, _, _, _, _) -> getCasesInPattern pat | _ -> [] - patMatchExpr.Clauses |> List.collect (getCasesInClause) |> Set.ofList + patMatchExpr.Clauses |> List.collect getCasesInClause |> Set.ofList let shouldGenerateUnionPatternMatchCases (patMatchExpr: PatternMatchExpr) (entity: FSharpEntity) = let caseCount = entity.UnionCases.Count @@ -504,7 +502,7 @@ let tryFindCaseInsertionParamsAtPos (codeGenService: ICodeGenerationService) pos let tryFindUnionDefinitionFromPos (codeGenService: ICodeGenerationService) pos document = asyncOption { let! patMatchExpr, insertionParams = tryFindCaseInsertionParamsAtPos codeGenService pos document - let! symbol, symbolUse = codeGenService.GetSymbolAndUseAtPositionOfKind(document.FullName, pos, SymbolKind.Ident) + let! _, symbolUse = codeGenService.GetSymbolAndUseAtPositionOfKind(document.FullName, pos, SymbolKind.Ident) let! superficialTypeDefinition = @@ -518,12 +516,12 @@ let tryFindUnionDefinitionFromPos (codeGenService: ICodeGenerationService) pos d | _ -> return None } - let! realTypeDefinition = superficialTypeDefinition + let! _ = superficialTypeDefinition let! realTypeDefinition = match superficialTypeDefinition with | Some(AbbreviatedType(TypeWithDefinition typeDef)) when typeDef.IsFSharpUnion -> Some typeDef - | Some(UnionType(_)) -> superficialTypeDefinition + | Some(UnionType _) -> superficialTypeDefinition | _ -> None return patMatchExpr, realTypeDefinition, insertionParams @@ -554,7 +552,7 @@ let private formatCase (ctxt: Context) (case: FSharpUnionCase) = // De-duplicate field names if there are conflicts let newFieldNames = Seq.unfold - (fun ((i, currentNamesWithIndices) as _state) -> + (fun (i, currentNamesWithIndices as _state) -> if i < fieldNames.Length then let name = fieldNames.[i] diff --git a/src/FsAutoComplete.Core/UnionPatternMatchCaseGenerator.fsi b/src/FsAutoComplete.Core/UnionPatternMatchCaseGenerator.fsi new file mode 100644 index 000000000..152063013 --- /dev/null +++ b/src/FsAutoComplete.Core/UnionPatternMatchCaseGenerator.fsi @@ -0,0 +1,36 @@ +/// Original code from VisualFSharpPowerTools project: https://github.com/fsprojects/VisualFSharpPowerTools/blob/master/src/FSharp.Editing/CodeGeneration/UnionPatternMatchCaseGenerator.fs +module FsAutoComplete.UnionPatternMatchCaseGenerator + +open FSharp.Compiler.Syntax +open FSharp.Compiler.Text +open FSharp.Compiler.Symbols + +[] +type PatternMatchExpr = + { + /// Range of 'match x with' or 'function' + MatchWithOrFunctionRange: Range + /// The whole pattern match expression + Expr: SynExpr + Clauses: SynMatchClause list + } + +[] +type UnionMatchCasesInsertionParams = + { InsertionPos: Position + IndentColumn: int } + +val shouldGenerateUnionPatternMatchCases: patMatchExpr: PatternMatchExpr -> entity: FSharpEntity -> bool + +val tryFindUnionDefinitionFromPos: + codeGenService: ICodeGenerationService -> + pos: Position -> + document: Document -> + Async<(PatternMatchExpr * FSharpEntity * UnionMatchCasesInsertionParams) option> + +val formatMatchExpr: + insertionParams: UnionMatchCasesInsertionParams -> + caseDefaultValue: string -> + patMatchExpr: PatternMatchExpr -> + entity: FSharpEntity -> + string diff --git a/src/FsAutoComplete.Core/UntypedAstUtils.fs b/src/FsAutoComplete.Core/UntypedAstUtils.fs index 59c5d9513..17c923bcc 100644 --- a/src/FsAutoComplete.Core/UntypedAstUtils.fs +++ b/src/FsAutoComplete.Core/UntypedAstUtils.fs @@ -28,61 +28,61 @@ module Syntax = type SyntaxCollectorBase() = abstract WalkSynModuleOrNamespace: SynModuleOrNamespace -> unit - default _.WalkSynModuleOrNamespace m = () + default _.WalkSynModuleOrNamespace _ = () abstract WalkAttribute: SynAttribute -> unit - default _.WalkAttribute a = () + default _.WalkAttribute _ = () abstract WalkSynModuleDecl: SynModuleDecl -> unit - default _.WalkSynModuleDecl m = () + default _.WalkSynModuleDecl _ = () abstract WalkExpr: SynExpr -> unit - default _.WalkExpr s = () + default _.WalkExpr _ = () abstract WalkTypar: SynTypar -> unit - default _.WalkTypar s = () + default _.WalkTypar _ = () abstract WalkTyparDecl: SynTyparDecl -> unit - default _.WalkTyparDecl s = () + default _.WalkTyparDecl _ = () abstract WalkTypeConstraint: SynTypeConstraint -> unit - default _.WalkTypeConstraint s = () + default _.WalkTypeConstraint _ = () abstract WalkType: SynType -> unit - default _.WalkType s = () + default _.WalkType _ = () abstract WalkMemberSig: SynMemberSig -> unit - default _.WalkMemberSig s = () + default _.WalkMemberSig _ = () abstract WalkPat: SynPat -> unit - default _.WalkPat s = () + default _.WalkPat _ = () abstract WalkValTyparDecls: SynValTyparDecls -> unit - default _.WalkValTyparDecls s = () + default _.WalkValTyparDecls _ = () abstract WalkBinding: SynBinding -> unit - default _.WalkBinding s = () + default _.WalkBinding _ = () abstract WalkSimplePat: SynSimplePat -> unit - default _.WalkSimplePat s = () + default _.WalkSimplePat _ = () abstract WalkInterfaceImpl: SynInterfaceImpl -> unit - default _.WalkInterfaceImpl s = () + default _.WalkInterfaceImpl _ = () abstract WalkClause: SynMatchClause -> unit - default _.WalkClause s = () + default _.WalkClause _ = () abstract WalkInterpolatedStringPart: SynInterpolatedStringPart -> unit - default _.WalkInterpolatedStringPart s = () + default _.WalkInterpolatedStringPart _ = () abstract WalkMeasure: SynMeasure -> unit - default _.WalkMeasure s = () + default _.WalkMeasure _ = () abstract WalkComponentInfo: SynComponentInfo -> unit - default _.WalkComponentInfo s = () + default _.WalkComponentInfo _ = () abstract WalkTypeDefnSigRepr: SynTypeDefnSigRepr -> unit - default _.WalkTypeDefnSigRepr s = () + default _.WalkTypeDefnSigRepr _ = () abstract WalkUnionCaseType: SynUnionCaseKind -> unit - default _.WalkUnionCaseType s = () + default _.WalkUnionCaseType _ = () abstract WalkEnumCase: SynEnumCase -> unit - default _.WalkEnumCase s = () + default _.WalkEnumCase _ = () abstract WalkField: SynField -> unit - default _.WalkField s = () + default _.WalkField _ = () abstract WalkTypeDefnSimple: SynTypeDefnSimpleRepr -> unit - default _.WalkTypeDefnSimple s = () + default _.WalkTypeDefnSimple _ = () abstract WalkValSig: SynValSig -> unit - default _.WalkValSig s = () + default _.WalkValSig _ = () abstract WalkMember: SynMemberDefn -> unit - default _.WalkMember s = () + default _.WalkMember _ = () abstract WalkUnionCase: SynUnionCase -> unit - default _.WalkUnionCase s = () + default _.WalkUnionCase _ = () abstract WalkTypeDefnRepr: SynTypeDefnRepr -> unit - default _.WalkTypeDefnRepr s = () + default _.WalkTypeDefnRepr _ = () abstract WalkTypeDefn: SynTypeDefn -> unit - default _.WalkTypeDefn s = () + default _.WalkTypeDefn _ = () let walkAst (walker: SyntaxCollectorBase) (input: ParsedInput) : unit = @@ -90,7 +90,7 @@ module Syntax = List.iter walkSynModuleOrNamespace moduleOrNamespaceList () - and walkSynModuleOrNamespace (SynModuleOrNamespace(decls = decls; attribs = AllAttrs attrs; range = r) as s) = + and walkSynModuleOrNamespace (SynModuleOrNamespace(decls = decls; attribs = AllAttrs attrs; range = _) as s) = walker.WalkSynModuleOrNamespace s List.iter walkAttribute attrs List.iter walkSynModuleDecl decls @@ -112,64 +112,64 @@ module Syntax = walker.WalkTypeConstraint s match s with - | SynTypeConstraint.WhereTyparIsValueType(t, r) - | SynTypeConstraint.WhereTyparIsReferenceType(t, r) - | SynTypeConstraint.WhereTyparIsUnmanaged(t, r) - | SynTypeConstraint.WhereTyparSupportsNull(t, r) - | SynTypeConstraint.WhereTyparIsComparable(t, r) - | SynTypeConstraint.WhereTyparIsEquatable(t, r) -> walkTypar t - | SynTypeConstraint.WhereTyparDefaultsToType(t, ty, r) - | SynTypeConstraint.WhereTyparSubtypeOfType(t, ty, r) -> + | SynTypeConstraint.WhereTyparIsValueType(t, _) + | SynTypeConstraint.WhereTyparIsReferenceType(t, _) + | SynTypeConstraint.WhereTyparIsUnmanaged(t, _) + | SynTypeConstraint.WhereTyparSupportsNull(t, _) + | SynTypeConstraint.WhereTyparIsComparable(t, _) + | SynTypeConstraint.WhereTyparIsEquatable(t, _) -> walkTypar t + | SynTypeConstraint.WhereTyparDefaultsToType(t, ty, _) + | SynTypeConstraint.WhereTyparSubtypeOfType(t, ty, _) -> walkTypar t walkType ty - | SynTypeConstraint.WhereTyparIsEnum(t, ts, r) - | SynTypeConstraint.WhereTyparIsDelegate(t, ts, r) -> + | SynTypeConstraint.WhereTyparIsEnum(t, ts, _) + | SynTypeConstraint.WhereTyparIsDelegate(t, ts, _) -> walkTypar t List.iter walkType ts - | SynTypeConstraint.WhereTyparSupportsMember(t, sign, r) -> + | SynTypeConstraint.WhereTyparSupportsMember(t, sign, _) -> walkType t walkMemberSig sign - | SynTypeConstraint.WhereSelfConstrained(t, r) -> walkType t + | SynTypeConstraint.WhereSelfConstrained(t, _) -> walkType t and walkPat s = walker.WalkPat s match s with - | SynPat.Tuple(elementPats = pats; range = r) - | SynPat.ArrayOrList(_, pats, r) - | SynPat.Ands(pats, r) -> List.iter walkPat pats - | SynPat.Named(ident, _, _, r) -> () - | SynPat.Typed(pat, t, r) -> + | SynPat.Tuple(elementPats = pats) + | SynPat.ArrayOrList(elementPats = pats) + | SynPat.Ands(pats = pats) -> List.iter walkPat pats + | SynPat.Named _ -> () + | SynPat.Typed(pat, t, _) -> walkPat pat walkType t - | SynPat.Attrib(pat, AllAttrs attrs, r) -> + | SynPat.Attrib(pat, AllAttrs attrs, _) -> walkPat pat List.iter walkAttribute attrs - | SynPat.Or(pat1, pat2, r, _) -> List.iter walkPat [ pat1; pat2 ] - | SynPat.LongIdent(typarDecls = typars; argPats = ConstructorPats pats; range = r) -> + | SynPat.Or(pat1, pat2, _, _) -> List.iter walkPat [ pat1; pat2 ] + | SynPat.LongIdent(typarDecls = typars; argPats = ConstructorPats pats; range = _) -> Option.iter walkSynValTyparDecls typars List.iter walkPat pats - | SynPat.Paren(pat, r) -> walkPat pat - | SynPat.IsInst(t, r) -> walkType t - | SynPat.QuoteExpr(e, r) -> walkExpr e - | SynPat.Const(_, r) -> () - | SynPat.Wild(r) -> () - | SynPat.Record(_, r) -> () - | SynPat.Null(r) -> () - | SynPat.OptionalVal(_, r) -> () - | SynPat.InstanceMember(_, _, _, accessibility, r) -> () - | SynPat.FromParseError(_, r) -> () - | SynPat.As(lpat, rpat, r) -> + | SynPat.Paren(pat, _) -> walkPat pat + | SynPat.IsInst(t, _) -> walkType t + | SynPat.QuoteExpr(e, _) -> walkExpr e + | SynPat.Const _ -> () + | SynPat.Wild _ -> () + | SynPat.Record _ -> () + | SynPat.Null _ -> () + | SynPat.OptionalVal _ -> () + | SynPat.InstanceMember _ -> () + | SynPat.FromParseError _ -> () + | SynPat.As(lpat, rpat, _) -> walkPat lpat walkPat rpat - | SynPat.ListCons(lpat, rpat, r, _) -> + | SynPat.ListCons(lpat, rpat, _, _) -> walkPat lpat walkPat rpat - and walkTypar (SynTypar(_, _, _) as s) = walker.WalkTypar s + and walkTypar (SynTypar _ as s) = walker.WalkTypar s and walkBinding - (SynBinding(attributes = AllAttrs attrs; headPat = pat; returnInfo = returnInfo; expr = e; range = r) as s) + (SynBinding(attributes = AllAttrs attrs; headPat = pat; returnInfo = returnInfo; expr = e; range = _) as s) = walker.WalkBinding s List.iter walkAttribute attrs @@ -177,14 +177,14 @@ module Syntax = walkExpr e returnInfo - |> Option.iter (fun (SynBindingReturnInfo(t, r, attrs, _)) -> + |> Option.iter (fun (SynBindingReturnInfo(t, _, attrs, _)) -> walkType t walkAttributes attrs) and walkAttributes (attrs: SynAttributes) = List.iter (fun (attrList: SynAttributeList) -> List.iter walkAttribute attrList.Attributes) attrs - and walkInterfaceImpl (SynInterfaceImpl(bindings = bindings; range = r) as s) = + and walkInterfaceImpl (SynInterfaceImpl(bindings = bindings; range = _) as s) = walker.WalkInterfaceImpl s List.iter walkBinding bindings @@ -192,43 +192,43 @@ module Syntax = walker.WalkType s match s with - | SynType.Array(_, t, r) - | SynType.HashConstraint(t, r) - | SynType.MeasurePower(t, _, r) -> walkType t - | SynType.Fun(t1, t2, r, _) -> + | SynType.Array(_, t, _) + | SynType.HashConstraint(t, _) + | SynType.MeasurePower(t, _, _) -> walkType t + | SynType.Fun(t1, t2, _, _) -> // | SynType.MeasureDivide(t1, t2, r) -> walkType t1 walkType t2 - | SynType.App(ty, _, types, _, _, _, r) -> + | SynType.App(ty, _, types, _, _, _, _) -> walkType ty List.iter walkType types - | SynType.LongIdentApp(_, _, _, types, _, _, r) -> List.iter walkType types - | SynType.Tuple(_, ts, r) -> + | SynType.LongIdentApp(_, _, _, types, _, _, _) -> List.iter walkType types + | SynType.Tuple(_, ts, _) -> ts |> List.iter (function | SynTupleTypeSegment.Type t -> walkType t | _ -> ()) - | SynType.WithGlobalConstraints(t, typeConstraints, r) -> + | SynType.WithGlobalConstraints(t, typeConstraints, _) -> walkType t List.iter walkTypeConstraint typeConstraints - | SynType.LongIdent(longDotId) -> () - | SynType.AnonRecd(isStruct, typeNames, r) -> () - | SynType.Var(genericName, r) -> () - | SynType.Anon(r) -> () - | SynType.StaticConstant(constant, r) -> () - | SynType.StaticConstantExpr(expr, r) -> () - | SynType.StaticConstantNamed(expr, _, r) -> () - | SynType.Paren(innerType, r) -> walkType innerType - | SynType.SignatureParameter(usedType = t; range = r) -> walkType t - | SynType.Or(lhs, rhs, r, _) -> + | SynType.LongIdent _ -> () + | SynType.AnonRecd _ -> () + | SynType.Var _ -> () + | SynType.Anon _ -> () + | SynType.StaticConstant _ -> () + | SynType.StaticConstantExpr _ -> () + | SynType.StaticConstantNamed _ -> () + | SynType.Paren(innerType, _) -> walkType innerType + | SynType.SignatureParameter(usedType = t; range = _) -> walkType t + | SynType.Or(lhs, rhs, _, _) -> walkType lhs walkType rhs - | SynType.FromParseError(r) -> () + | SynType.FromParseError _ -> () | SynType.Intersection(typar, types, _, _) -> Option.iter walkTypar typar List.iter walkType types - and walkClause (SynMatchClause(pat, e1, e2, r, _, _) as s) = + and walkClause (SynMatchClause(pat, e1, e2, _, _, _) as s) = walker.WalkClause s walkPat pat walkExpr e2 @@ -236,14 +236,14 @@ module Syntax = and walkSimplePats = function - | SynSimplePats.SimplePats(pats = pats; range = r) -> List.iter walkSimplePat pats + | SynSimplePats.SimplePats(pats = pats; range = _) -> List.iter walkSimplePat pats and walkInterpolatedStringPart s = walker.WalkInterpolatedStringPart s match s with - | SynInterpolatedStringPart.FillExpr(expr, ident) -> walkExpr expr - | SynInterpolatedStringPart.String(s, r) -> () + | SynInterpolatedStringPart.FillExpr(expr, _) -> walkExpr expr + | SynInterpolatedStringPart.String _ -> () and walkExpr s = walker.WalkExpr s @@ -266,115 +266,115 @@ module Syntax = | SynExpr.Quote(operator, _, quotedExpr, _, _) -> walkExpr operator walkExpr quotedExpr - | SynExpr.SequentialOrImplicitYield(_, e1, e2, ifNotE, r) -> + | SynExpr.SequentialOrImplicitYield(_, e1, e2, ifNotE, _) -> walkExpr e1 walkExpr e2 walkExpr ifNotE - | SynExpr.Lambda(args = pats; body = e; range = r) -> + | SynExpr.Lambda(args = pats; body = e; range = _) -> walkSimplePats pats walkExpr e - | SynExpr.New(_, t, e, r) - | SynExpr.TypeTest(e, t, r) - | SynExpr.Upcast(e, t, r) - | SynExpr.Downcast(e, t, r) -> + | SynExpr.New(_, t, e, _) + | SynExpr.TypeTest(e, t, _) + | SynExpr.Upcast(e, t, _) + | SynExpr.Downcast(e, t, _) -> walkExpr e walkType t | SynExpr.Tuple(_, es, _, _) | Sequentials es -> List.iter walkExpr es //TODO?? - | SynExpr.ArrayOrList(_, es, r) -> List.iter walkExpr es - | SynExpr.App(_, _, e1, e2, r) - | SynExpr.TryFinally(e1, e2, r, _, _, _) - | SynExpr.While(_, e1, e2, r) -> List.iter walkExpr [ e1; e2 ] - | SynExpr.Record(_, _, fields, r) -> + | SynExpr.ArrayOrList(_, es, _) -> List.iter walkExpr es + | SynExpr.App(_, _, e1, e2, _) + | SynExpr.TryFinally(e1, e2, _, _, _, _) + | SynExpr.While(_, e1, e2, _) -> List.iter walkExpr [ e1; e2 ] + | SynExpr.Record(_, _, fields, _) -> fields - |> List.iter (fun (SynExprRecordField(fieldName = (ident, _); expr = e)) -> e |> Option.iter walkExpr) - | SynExpr.ObjExpr(ty, argOpt, _, bindings, _, ifaces, _, r) -> + |> List.iter (fun (SynExprRecordField(fieldName = (_, _); expr = e)) -> e |> Option.iter walkExpr) + | SynExpr.ObjExpr(ty, argOpt, _, bindings, _, ifaces, _, _) -> - argOpt |> Option.iter (fun (e, ident) -> walkExpr e) + argOpt |> Option.iter (fun (e, _) -> walkExpr e) walkType ty List.iter walkBinding bindings List.iter walkInterfaceImpl ifaces - | SynExpr.For(identBody = e1; toBody = e2; doBody = e3; range = r) -> List.iter walkExpr [ e1; e2; e3 ] - | SynExpr.ForEach(_, _, _, _, pat, e1, e2, r) -> + | SynExpr.For(identBody = e1; toBody = e2; doBody = e3; range = _) -> List.iter walkExpr [ e1; e2; e3 ] + | SynExpr.ForEach(_, _, _, _, pat, e1, e2, _) -> walkPat pat List.iter walkExpr [ e1; e2 ] - | SynExpr.MatchLambda(_, _, synMatchClauseList, _, r) -> List.iter walkClause synMatchClauseList - | SynExpr.Match(expr = e; clauses = synMatchClauseList; range = r) -> + | SynExpr.MatchLambda(_, _, synMatchClauseList, _, _) -> List.iter walkClause synMatchClauseList + | SynExpr.Match(expr = e; clauses = synMatchClauseList; range = _) -> walkExpr e List.iter walkClause synMatchClauseList - | SynExpr.TypeApp(e, _, tys, _, _, tr, r) -> + | SynExpr.TypeApp(e, _, tys, _, _, _, _) -> List.iter walkType tys walkExpr e - | SynExpr.LetOrUse(bindings = bindings; body = e; range = r) -> + | SynExpr.LetOrUse(bindings = bindings; body = e; range = _) -> List.iter walkBinding bindings walkExpr e - | SynExpr.TryWith(tryExpr = e; withCases = clauses; range = r) -> + | SynExpr.TryWith(tryExpr = e; withCases = clauses; range = _) -> List.iter walkClause clauses walkExpr e - | SynExpr.IfThenElse(ifExpr = e1; thenExpr = e2; elseExpr = e3; range = r) -> + | SynExpr.IfThenElse(ifExpr = e1; thenExpr = e2; elseExpr = e3; range = _) -> List.iter walkExpr [ e1; e2 ] e3 |> Option.iter walkExpr - | SynExpr.LongIdentSet(ident, e, r) - | SynExpr.DotGet(e, _, ident, r) -> walkExpr e - | SynExpr.DotSet(e1, idents, e2, r) -> + | SynExpr.LongIdentSet(_, e, _) + | SynExpr.DotGet(e, _, _, _) -> walkExpr e + | SynExpr.DotSet(e1, _, e2, _) -> walkExpr e1 walkExpr e2 - | SynExpr.DotIndexedGet(e, args, _, r) -> + | SynExpr.DotIndexedGet(e, args, _, _) -> walkExpr e walkExpr args - | SynExpr.DotIndexedSet(e1, args, e2, _, _, r) -> + | SynExpr.DotIndexedSet(e1, args, e2, _, _, _) -> walkExpr e1 walkExpr args walkExpr e2 - | SynExpr.NamedIndexedPropertySet(ident, e1, e2, r) -> List.iter walkExpr [ e1; e2 ] - | SynExpr.DotNamedIndexedPropertySet(e1, ident, e2, e3, r) -> List.iter walkExpr [ e1; e2; e3 ] - | SynExpr.JoinIn(e1, _, e2, r) -> List.iter walkExpr [ e1; e2 ] - | SynExpr.LetOrUseBang(pat = pat; rhs = e1; andBangs = ands; body = e2; range = r) -> + | SynExpr.NamedIndexedPropertySet(_, e1, e2, _) -> List.iter walkExpr [ e1; e2 ] + | SynExpr.DotNamedIndexedPropertySet(e1, _, e2, e3, _) -> List.iter walkExpr [ e1; e2; e3 ] + | SynExpr.JoinIn(e1, _, e2, _) -> List.iter walkExpr [ e1; e2 ] + | SynExpr.LetOrUseBang(pat = pat; rhs = e1; andBangs = ands; body = e2; range = _) -> walkPat pat walkExpr e1 - for (SynExprAndBang(pat = pat; body = body; range = r)) in ands do + for SynExprAndBang(pat = pat; body = body; range = _) in ands do walkPat pat walkExpr body walkExpr e2 - | SynExpr.TraitCall(t, sign, e, r) -> + | SynExpr.TraitCall(t, sign, e, _) -> walkType t walkMemberSig sign walkExpr e - | SynExpr.Const(SynConst.Measure(synMeasure = m), r) -> walkMeasure m - | SynExpr.Const(_, r) -> () - | SynExpr.AnonRecd(isStruct, copyInfo, recordFields, r, trivia) -> () - | SynExpr.Sequential(seqPoint, isTrueSeq, expr1, expr2, r) -> () - | SynExpr.Ident(_) -> () - | SynExpr.LongIdent(isOptional, longDotId, altNameRefCell, r) -> () - | SynExpr.Set(_, _, r) -> () - | SynExpr.Null(r) -> () - | SynExpr.ImplicitZero(r) -> () - | SynExpr.MatchBang(range = r) -> () - | SynExpr.LibraryOnlyILAssembly(_, _, _, _, r) -> () - | SynExpr.LibraryOnlyStaticOptimization(_, _, _, r) -> () - | SynExpr.LibraryOnlyUnionCaseFieldGet(expr, longId, _, r) -> () - | SynExpr.LibraryOnlyUnionCaseFieldSet(_, longId, _, _, r) -> () - | SynExpr.ArbitraryAfterError(debugStr, r) -> () - | SynExpr.FromParseError(expr, r) -> () - | SynExpr.DiscardAfterMissingQualificationAfterDot(_, _, r) -> () - | SynExpr.Fixed(expr, r) -> () - | SynExpr.InterpolatedString(parts, kind, r) -> + | SynExpr.Const(SynConst.Measure(synMeasure = m), _) -> walkMeasure m + | SynExpr.Const _ -> () + | SynExpr.AnonRecd _ -> () + | SynExpr.Sequential _ -> () + | SynExpr.Ident _ -> () + | SynExpr.LongIdent _ -> () + | SynExpr.Set _ -> () + | SynExpr.Null _ -> () + | SynExpr.ImplicitZero _ -> () + | SynExpr.MatchBang(range = _) -> () + | SynExpr.LibraryOnlyILAssembly _ -> () + | SynExpr.LibraryOnlyStaticOptimization _ -> () + | SynExpr.LibraryOnlyUnionCaseFieldGet _ -> () + | SynExpr.LibraryOnlyUnionCaseFieldSet _ -> () + | SynExpr.ArbitraryAfterError _ -> () + | SynExpr.FromParseError _ -> () + | SynExpr.DiscardAfterMissingQualificationAfterDot _ -> () + | SynExpr.Fixed _ -> () + | SynExpr.InterpolatedString(parts, _, _) -> for part in parts do walkInterpolatedStringPart part - | SynExpr.IndexFromEnd(itemExpr, r) -> walkExpr itemExpr - | SynExpr.IndexRange(e1, _, e2, _, _, r) -> + | SynExpr.IndexFromEnd(itemExpr, _) -> walkExpr itemExpr + | SynExpr.IndexRange(e1, _, e2, _, _, _) -> Option.iter walkExpr e1 Option.iter walkExpr e2 | SynExpr.DebugPoint(innerExpr = expr) -> walkExpr expr - | SynExpr.Dynamic(funcExpr = e1; argExpr = e2; range = range) -> + | SynExpr.Dynamic(funcExpr = e1; argExpr = e2; range = _) -> walkExpr e1 walkExpr e2 - | SynExpr.Typar(t, r) -> walkTypar t + | SynExpr.Typar(t, _) -> walkTypar t | SynExpr.WhileBang(whileExpr = whileExpr; doExpr = doExpr) -> walkExpr whileExpr walkExpr doExpr @@ -386,36 +386,36 @@ module Syntax = | SynMeasure.Product(measure1 = m1; measure2 = m2) -> walkMeasure m1 walkMeasure m2 - | SynMeasure.Divide(m1, _, m2, r) -> + | SynMeasure.Divide(m1, _, m2, _) -> Option.iter walkMeasure m1 walkMeasure m2 - | SynMeasure.Named(longIdent, r) -> () - | SynMeasure.Seq(ms, r) -> List.iter walkMeasure ms - | SynMeasure.Power(m, _, _, r) -> walkMeasure m - | SynMeasure.Var(ty, r) -> walkTypar ty - | SynMeasure.Paren(m, r) -> walkMeasure m - | SynMeasure.One(_) - | SynMeasure.Anon(_) -> () + | SynMeasure.Named _ -> () + | SynMeasure.Seq(ms, _) -> List.iter walkMeasure ms + | SynMeasure.Power(m, _, _, _) -> walkMeasure m + | SynMeasure.Var(ty, _) -> walkTypar ty + | SynMeasure.Paren(m, _) -> walkMeasure m + | SynMeasure.One _ + | SynMeasure.Anon _ -> () and walkSimplePat s = walker.WalkSimplePat s match s with - | SynSimplePat.Attrib(pat, AllAttrs attrs, r) -> + | SynSimplePat.Attrib(pat, AllAttrs attrs, _) -> walkSimplePat pat List.iter walkAttribute attrs - | SynSimplePat.Typed(pat, t, r) -> + | SynSimplePat.Typed(pat, t, _) -> walkSimplePat pat walkType t - | SynSimplePat.Id(ident, altNameRefCell, isCompilerGenerated, isThisVar, isOptArg, r) -> () + | SynSimplePat.Id _ -> () - and walkField (SynField(attributes = AllAttrs attrs; fieldType = t; range = r) as s) = + and walkField (SynField(attributes = AllAttrs attrs; fieldType = t; range = _) as s) = walker.WalkField s List.iter walkAttribute attrs walkType t and walkValSig - (SynValSig(attributes = AllAttrs attrs; synType = t; arity = SynValInfo(argInfos, argInfo); range = r) as s) + (SynValSig(attributes = AllAttrs attrs; synType = t; arity = SynValInfo(argInfos, argInfo); range = _) as s) = walker.WalkValSig s List.iter walkAttribute attrs @@ -429,11 +429,11 @@ module Syntax = walker.WalkMemberSig s match s with - | SynMemberSig.Inherit(t, r) - | SynMemberSig.Interface(t, r) -> walkType t - | SynMemberSig.Member(vs, _, r, _) -> walkValSig vs - | SynMemberSig.ValField(f, r) -> walkField f - | SynMemberSig.NestedType(SynTypeDefnSig(typeInfo = info; typeRepr = repr; members = memberSigs), r) -> + | SynMemberSig.Inherit(t, _) + | SynMemberSig.Interface(t, _) -> walkType t + | SynMemberSig.Member(vs, _, _, _) -> walkValSig vs + | SynMemberSig.ValField(f, _) -> walkField f + | SynMemberSig.NestedType(SynTypeDefnSig(typeInfo = info; typeRepr = repr; members = memberSigs), _) -> walkComponentInfo info walkTypeDefnSigRepr repr @@ -443,31 +443,31 @@ module Syntax = walker.WalkMember s match s with - | SynMemberDefn.AbstractSlot(valSig, _, r, _) -> walkValSig valSig - | SynMemberDefn.Member(binding, r) -> walkBinding binding - | SynMemberDefn.ImplicitCtor(_, AllAttrs attrs, AllSimplePats pats, _, _, r, _) -> + | SynMemberDefn.AbstractSlot(valSig, _, _, _) -> walkValSig valSig + | SynMemberDefn.Member(binding, _) -> walkBinding binding + | SynMemberDefn.ImplicitCtor(_, AllAttrs attrs, AllSimplePats pats, _, _, _, _) -> List.iter walkAttribute attrs List.iter walkSimplePat pats - | SynMemberDefn.ImplicitInherit(t, e, _, r) -> + | SynMemberDefn.ImplicitInherit(t, e, _, _) -> walkType t walkExpr e - | SynMemberDefn.LetBindings(bindings, _, _, r) -> List.iter walkBinding bindings - | SynMemberDefn.Interface(t, _, members, r) -> + | SynMemberDefn.LetBindings(bindings, _, _, _) -> List.iter walkBinding bindings + | SynMemberDefn.Interface(t, _, members, _) -> walkType t members |> Option.iter (List.iter walkMember) - | SynMemberDefn.Inherit(t, _, r) -> walkType t - | SynMemberDefn.ValField(field, r) -> walkField field - | SynMemberDefn.NestedType(tdef, _, r) -> walkTypeDefn tdef - | SynMemberDefn.AutoProperty(attributes = AllAttrs attrs; typeOpt = t; synExpr = e; range = r) -> + | SynMemberDefn.Inherit(t, _, _) -> walkType t + | SynMemberDefn.ValField(field, _) -> walkField field + | SynMemberDefn.NestedType(tdef, _, _) -> walkTypeDefn tdef + | SynMemberDefn.AutoProperty(attributes = AllAttrs attrs; typeOpt = t; synExpr = e; range = _) -> List.iter walkAttribute attrs Option.iter walkType t walkExpr e - | SynMemberDefn.Open(longId, r) -> () - | SynMemberDefn.GetSetMember(memberDefnForGet = getter; memberDefnForSet = setter; range = range) -> + | SynMemberDefn.Open _ -> () + | SynMemberDefn.GetSetMember(memberDefnForGet = getter; memberDefnForSet = setter; range = _) -> Option.iter walkBinding getter Option.iter walkBinding setter - and walkEnumCase (SynEnumCase(attributes = AllAttrs attrs; range = r) as s) = + and walkEnumCase (SynEnumCase(attributes = AllAttrs attrs; range = _) as s) = walker.WalkEnumCase s List.iter walkAttribute attrs @@ -478,7 +478,7 @@ module Syntax = | SynUnionCaseKind.Fields fields -> List.iter walkField fields | SynUnionCaseKind.FullType(t, _) -> walkType t - and walkUnionCase (SynUnionCase(attributes = AllAttrs attrs; caseType = t; range = r) as s) = + and walkUnionCase (SynUnionCase(attributes = AllAttrs attrs; caseType = t; range = _) as s) = walker.WalkUnionCase s List.iter walkAttribute attrs walkUnionCaseType t @@ -487,18 +487,18 @@ module Syntax = walker.WalkTypeDefnSimple s match s with - | SynTypeDefnSimpleRepr.Enum(cases, r) -> List.iter walkEnumCase cases - | SynTypeDefnSimpleRepr.Union(_, cases, r) -> List.iter walkUnionCase cases - | SynTypeDefnSimpleRepr.Record(_, fields, r) -> List.iter walkField fields - | SynTypeDefnSimpleRepr.TypeAbbrev(_, t, r) -> walkType t - | SynTypeDefnSimpleRepr.General(_, _, _, _, _, _, _, r) -> () - | SynTypeDefnSimpleRepr.LibraryOnlyILAssembly(_, r) -> () - | SynTypeDefnSimpleRepr.None(r) -> () - | SynTypeDefnSimpleRepr.Exception(_) -> () + | SynTypeDefnSimpleRepr.Enum(cases, _) -> List.iter walkEnumCase cases + | SynTypeDefnSimpleRepr.Union(_, cases, _) -> List.iter walkUnionCase cases + | SynTypeDefnSimpleRepr.Record(_, fields, _) -> List.iter walkField fields + | SynTypeDefnSimpleRepr.TypeAbbrev(_, t, _) -> walkType t + | SynTypeDefnSimpleRepr.General _ -> () + | SynTypeDefnSimpleRepr.LibraryOnlyILAssembly _ -> () + | SynTypeDefnSimpleRepr.None _ -> () + | SynTypeDefnSimpleRepr.Exception _ -> () and walkComponentInfo (SynComponentInfo( - attributes = AllAttrs attrs; typeParams = typars; constraints = constraints; longId = longIdent; range = r) as s) + attributes = AllAttrs attrs; typeParams = typars; constraints = constraints; longId = _; range = _) as s) = walker.WalkComponentInfo s List.iter walkAttribute attrs @@ -509,8 +509,8 @@ module Syntax = walker.WalkTypeDefnRepr s match s with - | SynTypeDefnRepr.ObjectModel(_, defns, r) -> List.iter walkMember defns - | SynTypeDefnRepr.Simple(defn, r) -> walkTypeDefnSimple defn + | SynTypeDefnRepr.ObjectModel(_, defns, _) -> List.iter walkMember defns + | SynTypeDefnRepr.Simple(defn, _) -> walkTypeDefnSimple defn | SynTypeDefnRepr.Exception _ -> () and walkTypeDefnSigRepr s = @@ -521,7 +521,7 @@ module Syntax = | SynTypeDefnSigRepr.Simple(defn, _) -> walkTypeDefnSimple defn | SynTypeDefnSigRepr.Exception _ -> () - and walkTypeDefn (SynTypeDefn(info, repr, members, implicitCtor, r, _) as s) = + and walkTypeDefn (SynTypeDefn(info, repr, members, implicitCtor, _, _) as s) = walker.WalkTypeDefn s walkComponentInfo info @@ -534,17 +534,17 @@ module Syntax = match decl with | SynModuleDecl.NamespaceFragment fragment -> walkSynModuleOrNamespace fragment - | SynModuleDecl.NestedModule(info, _, modules, _, r, _) -> + | SynModuleDecl.NestedModule(info, _, modules, _, _, _) -> walkComponentInfo info List.iter walkSynModuleDecl modules - | SynModuleDecl.Let(_, bindings, r) -> List.iter walkBinding bindings - | SynModuleDecl.Expr(expr, r) -> walkExpr expr - | SynModuleDecl.Types(types, r) -> List.iter walkTypeDefn types - | SynModuleDecl.Attributes(attributes = AllAttrs attrs; range = r) -> List.iter walkAttribute attrs - | SynModuleDecl.ModuleAbbrev(ident, longId, r) -> () - | SynModuleDecl.Exception(_, r) -> () - | SynModuleDecl.Open(longDotId, r) -> () - | SynModuleDecl.HashDirective(_, r) -> () + | SynModuleDecl.Let(_, bindings, _) -> List.iter walkBinding bindings + | SynModuleDecl.Expr(expr, _) -> walkExpr expr + | SynModuleDecl.Types(types, _) -> List.iter walkTypeDefn types + | SynModuleDecl.Attributes(attributes = AllAttrs attrs; range = _) -> List.iter walkAttribute attrs + | SynModuleDecl.ModuleAbbrev _ -> () + | SynModuleDecl.Exception _ -> () + | SynModuleDecl.Open _ -> () + | SynModuleDecl.HashDirective _ -> () match input with @@ -556,10 +556,7 @@ namespace FsAutoComplete module UntypedAstUtils = open FSharp.Compiler.Syntax - open System.Collections.Generic - open FSharp.Compiler open FSharp.Compiler.Text - open FSharp.Control.Reactive.Observable type Range with @@ -612,7 +609,7 @@ module FoldingRange = override _.WalkInterpolatedStringPart i = match i with | SynInterpolatedStringPart.FillExpr(qualifiers = Some ident) -> addIfInside ident.idRange - | SynInterpolatedStringPart.String(s, r) -> addIfInside r + | SynInterpolatedStringPart.String(_, r) -> addIfInside r | _ -> () override _.WalkExpr e = addIfInside e.Range @@ -646,7 +643,7 @@ module FoldingRange = member _.Ranges = ranges let getRangesAtPosition input (r: Position) : Range list = - let walker = new RangeCollectorWalker(r) + let walker = RangeCollectorWalker(r) walkAst walker input walker.Ranges |> Seq.toList @@ -683,7 +680,7 @@ module Completion = | SynInterpolatedStringPart.FillExpr(e, _) -> Range.rangeContainsPos e.Range pos match part with - | SynInterpolatedStringPart.String(s, m) when Range.rangeContainsPos m pos && not inRangeOfPrevious -> + | SynInterpolatedStringPart.String(_, m) when Range.rangeContainsPos m pos && not inRangeOfPrevious -> Some Context.StringLiteral | SynInterpolatedStringPart.String _ -> None | SynInterpolatedStringPart.FillExpr(e, _) when diff --git a/src/FsAutoComplete.Core/Utils.fs b/src/FsAutoComplete.Core/Utils.fs index e49e7196d..f5bb4d3c9 100644 --- a/src/FsAutoComplete.Core/Utils.fs +++ b/src/FsAutoComplete.Core/Utils.fs @@ -445,9 +445,12 @@ module String = let (|StartsWith|_|) (pattern: string) (value: string) = - if String.IsNullOrWhiteSpace value then None - elif value.StartsWith pattern then Some() - else None + if String.IsNullOrWhiteSpace value then + None + elif value.StartsWith(pattern, StringComparison.Ordinal) then + Some() + else + None let split (splitter: char) (s: string) = s.Split([| splitter |], StringSplitOptions.RemoveEmptyEntries) |> List.ofArray @@ -461,7 +464,7 @@ module String = yield line.Value line.Value <- reader.ReadLine() - if str.EndsWith("\n") then + if str.EndsWith("\n", StringComparison.Ordinal) then // last trailing space not returned // http://stackoverflow.com/questions/19365404/stringreader-omits-trailing-linebreak yield String.Empty |] @@ -638,7 +641,7 @@ let inline fail msg = Printf.kprintf Debug.Fail msg let chooseByPrefix (prefix: string) (s: string) = - if s.StartsWith(prefix) then + if s.StartsWith(prefix, StringComparison.Ordinal) then Some(s.Substring(prefix.Length)) else None @@ -646,7 +649,7 @@ let chooseByPrefix (prefix: string) (s: string) = let chooseByPrefix2 prefixes (s: string) = prefixes |> List.tryPick (fun prefix -> chooseByPrefix prefix s) let splitByPrefix (prefix: string) (s: string) = - if s.StartsWith(prefix) then + if s.StartsWith(prefix, StringComparison.Ordinal) then Some(prefix, s.Substring(prefix.Length)) else None @@ -659,7 +662,7 @@ module Patterns = let (|StartsWith|_|) (pat: string) (str: string) = match str with | null -> None - | _ when str.StartsWith pat -> Some str + | _ when str.StartsWith(pat, StringComparison.Ordinal) -> Some str | _ -> None let (|Contains|_|) (pat: string) (str: string) = diff --git a/src/FsAutoComplete.Core/paket.references b/src/FsAutoComplete.Core/paket.references index 12fbc304d..7e54bf3cb 100644 --- a/src/FsAutoComplete.Core/paket.references +++ b/src/FsAutoComplete.Core/paket.references @@ -1,17 +1,12 @@ -FSharp.Compiler.Service FSharp.Analyzers.SDK -Newtonsoft.Json ICSharpCode.Decompiler Microsoft.SourceLink.GitHub -System.Configuration.ConfigurationManager FSharp.UMX FsToolkit.ErrorHandling.TaskResult Fantomas.Client FSharp.Data.Adaptive IcedTasks - Ionide.ProjInfo.ProjectSystem -System.Reflection.Metadata Microsoft.Build.Utilities.Core Ionide.LanguageServerProtocol Ionide.KeepAChangelog.Tasks @@ -19,3 +14,5 @@ Microsoft.Extensions.Caching.Memory Microsoft.CodeAnalysis LinkDotNet.StringBuilder CommunityToolkit.HighPerformance +Ionide.Analyzers +FSharp.Analyzers.Build diff --git a/src/FsAutoComplete.Logging/FsLibLog.fs b/src/FsAutoComplete.Logging/FsLibLog.fs index ffcbb5bd6..e45504d8c 100644 --- a/src/FsAutoComplete.Logging/FsLibLog.fs +++ b/src/FsAutoComplete.Logging/FsLibLog.fs @@ -878,7 +878,7 @@ module Providers = true | None -> loggerGateway.Value.IsEnabled logger microsoftLevel - member this.OpenMappedContext (key: string) (value: obj) (destructure: bool) : IDisposable = + member this.OpenMappedContext (key: string) (value: obj) (_destructure: bool) : IDisposable = match microsoftLoggerFactory with | None -> { new IDisposable with diff --git a/src/FsAutoComplete.Logging/FsOpenTelemetry.fs b/src/FsAutoComplete.Logging/FsOpenTelemetry.fs index b10b300d7..ad467cc68 100644 --- a/src/FsAutoComplete.Logging/FsOpenTelemetry.fs +++ b/src/FsAutoComplete.Logging/FsOpenTelemetry.fs @@ -6,7 +6,6 @@ open System open System.Diagnostics open System.Runtime.CompilerServices open System.Collections.Generic -open Microsoft.FSharp.Quotations.Patterns // Thanks https://github.com/fsprojects/FSharp.UMX @@ -604,9 +603,7 @@ type ActivityExtensions = /// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/exceptions.md#semantic-conventions-for-exceptions /// The span to add the error information to - /// The exception message. - /// The type of the exception (its fully-qualified class name, if applicable). The dynamic type of the exception should be preferred over the static type in languages that support it. - /// A stacktrace as a string in the natural representation for the language runtime. The representation is to be determined and documented by each language SIG. + /// The exception message. /// SHOULD be set to true if the exception event is recorded at a point where it is known that the exception is escaping the scope of the span. [] static member inline RecordExceptions(span: Activity, e: exn, ?escaped: bool) = @@ -659,7 +656,7 @@ type ActivitySourceExtensions = ?startTime: DateTimeOffset, [] ?memberName: string, [] ?path: string, - [] ?line: int + [] ?line: int ) = let name_space = @@ -699,7 +696,6 @@ type ActivitySourceExtensions = /// This should be used by methods in classes. Creates and starts a new System.Diagnostics.Activity object if there is any listener to the Activity events, returns null otherwise. /// Provides APIs to create and start System.Diagnostics.Activity objects. /// The type where the trace is located. - /// The namespace where this code is located. /// The System.Diagnostics.ActivityKind /// The parent System.Diagnostics.ActivityContext object to initialize the created Activity object with. /// The optional tags list to initialize the created Activity object with. @@ -721,7 +717,7 @@ type ActivitySourceExtensions = ?startTime: DateTimeOffset, [] ?memberName: string, [] ?path: string, - [] ?line: int + [] ?line: int ) = let name_space = ty.FullName let name = $"{name_space}.{memberName.Value}" @@ -763,7 +759,7 @@ type ActivitySourceExtensions = ?startTime: DateTimeOffset, [] ?memberName: string, [] ?path: string, - [] ?line: int + [] ?line: int ) = let ty = typeof<'typAr> @@ -801,7 +797,7 @@ type ActivitySourceExtensions = ?startTime: DateTimeOffset, [] ?memberName: string, [] ?path: string, - [] ?line: int + [] ?line: int ) = tracer.StartActivityExt( diff --git a/src/FsAutoComplete.Logging/paket.references b/src/FsAutoComplete.Logging/paket.references index 462a46022..4b44cf4dc 100644 --- a/src/FsAutoComplete.Logging/paket.references +++ b/src/FsAutoComplete.Logging/paket.references @@ -2,3 +2,5 @@ FSharp.Core Microsoft.NETFramework.ReferenceAssemblies Ionide.KeepAChangelog.Tasks +Ionide.Analyzers +FSharp.Analyzers.Build diff --git a/src/FsAutoComplete/CodeFixes.fs b/src/FsAutoComplete/CodeFixes.fs index 77ac654bb..3c11fdd3c 100644 --- a/src/FsAutoComplete/CodeFixes.fs +++ b/src/FsAutoComplete/CodeFixes.fs @@ -326,6 +326,9 @@ module Run = let ifDiagnosticByMessage (checkMessage: string) handler : CodeFix = runDiagnostics (fun d -> d.Message.Contains checkMessage) handler + let ifDiagnosticByCheckMessage (checkMessageFunc: (string -> bool) list) handler : CodeFix = + runDiagnostics (fun d -> checkMessageFunc |> List.exists (fun f -> f d.Message)) handler + let ifDiagnosticByType (diagnosticType: string) handler : CodeFix = runDiagnostics (fun d -> diff --git a/src/FsAutoComplete/CodeFixes.fsi b/src/FsAutoComplete/CodeFixes.fsi index fd300841f..1a8366bbe 100644 --- a/src/FsAutoComplete/CodeFixes.fsi +++ b/src/FsAutoComplete/CodeFixes.fsi @@ -162,6 +162,11 @@ module Run = handler: (Diagnostic -> CodeActionParams -> Async>) -> (CodeActionParams -> Async>) + val ifDiagnosticByCheckMessage: + checkMessageFunc: (string -> bool) list -> + handler: (Diagnostic -> CodeActionParams -> Async>) -> + (CodeActionParams -> Async>) + val ifDiagnosticByType: diagnosticType: string -> handler: (Diagnostic -> CodeActionParams -> Async>) -> diff --git a/src/FsAutoComplete/CodeFixes/AddNewKeywordToDisposableConstructorInvocation.fs b/src/FsAutoComplete/CodeFixes/AddNewKeywordToDisposableConstructorInvocation.fs index 15ae9f31e..ef8e2e471 100644 --- a/src/FsAutoComplete/CodeFixes/AddNewKeywordToDisposableConstructorInvocation.fs +++ b/src/FsAutoComplete/CodeFixes/AddNewKeywordToDisposableConstructorInvocation.fs @@ -10,7 +10,7 @@ open FsAutoComplete.LspHelpers let title = "Add 'new'" /// a codefix that suggests using the 'new' keyword on IDisposables -let fix (getRangeText: GetRangeText) = +let fix = Run.ifDiagnosticByCode (Set.ofList [ "760" ]) (fun diagnostic codeActionParams -> AsyncResult.retn [ { SourceDiagnostic = Some diagnostic diff --git a/src/FsAutoComplete/CodeFixes/AddNewKeywordToDisposableConstructorInvocation.fsi b/src/FsAutoComplete/CodeFixes/AddNewKeywordToDisposableConstructorInvocation.fsi index ca573f2f6..390897ea4 100644 --- a/src/FsAutoComplete/CodeFixes/AddNewKeywordToDisposableConstructorInvocation.fsi +++ b/src/FsAutoComplete/CodeFixes/AddNewKeywordToDisposableConstructorInvocation.fsi @@ -1,12 +1,8 @@ module FsAutoComplete.CodeFix.AddNewKeywordToDisposableConstructorInvocation -open FsToolkit.ErrorHandling -open FsAutoComplete.CodeFix open FsAutoComplete.CodeFix.Types open Ionide.LanguageServerProtocol.Types -open FsAutoComplete -open FsAutoComplete.LspHelpers val title: string /// a codefix that suggests using the 'new' keyword on IDisposables -val fix: getRangeText: GetRangeText -> (CodeActionParams -> Async>) +val fix: (CodeActionParams -> Async>) diff --git a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs index 981f390c2..626ec2beb 100644 --- a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs +++ b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs @@ -698,7 +698,8 @@ module private CommonFixes = /// -> Prepend space if leading sign in `replacement` and operator char immediately in front (in `lineStr`) let prependSpaceIfNecessary (range: Range) (lineStr: string) (replacement: string) = if - (replacement.StartsWith "-" || replacement.StartsWith "+") + (replacement.StartsWith("-", StringComparison.Ordinal) + || replacement.StartsWith("+", StringComparison.Ordinal)) && range.Start.Character > 0 && "!$%&*+-./<=>?@^|~".Contains(lineStr[range.Start.Character - 1]) then @@ -771,7 +772,6 @@ module private CommonFixes = let replaceWithNamedConstantFix doc (pos: FcsPos) - (lineStr: String) (parseAndCheck: ParseAndCheckResults) (constant: SynConst) (constantRange: Range) @@ -831,7 +831,6 @@ module private CommonFixes = replaceWithNamedConstantFix doc pos - lineStr parseAndCheck constant constantRange @@ -841,7 +840,6 @@ module private CommonFixes = replaceWithNamedConstantFix doc pos - lineStr parseAndCheck constant constantRange @@ -851,7 +849,6 @@ module private CommonFixes = replaceWithNamedConstantFix doc pos - lineStr parseAndCheck constant constantRange @@ -870,7 +867,6 @@ module private CommonFixes = replaceWithNamedConstantFix doc pos - lineStr parseAndCheck constant constantRange @@ -880,7 +876,6 @@ module private CommonFixes = replaceWithNamedConstantFix doc pos - lineStr parseAndCheck constant constantRange @@ -890,7 +885,6 @@ module private CommonFixes = replaceWithNamedConstantFix doc pos - lineStr parseAndCheck constant constantRange @@ -903,7 +897,6 @@ module private CommonFixes = replaceWithNamedConstantFix doc pos - lineStr parseAndCheck constant constantRange @@ -913,7 +906,6 @@ module private CommonFixes = replaceWithNamedConstantFix doc pos - lineStr parseAndCheck constant constantRange @@ -938,7 +930,7 @@ module private CharFix = mkFix doc data [||] - let convertToOtherFormatFixes doc (lineStr: String) (constant: CharConstant) = + let convertToOtherFormatFixes doc (constant: CharConstant) = [ let mkFix' title replacement = let edits = [| { Range = constant.ValueRange.ToRangeInside constant.Range @@ -989,7 +981,7 @@ module private CharFix = let all doc (lineStr: String) (error: bool) (constant: CharConstant) = [ if not error then - yield! convertToOtherFormatFixes doc lineStr constant + yield! convertToOtherFormatFixes doc constant if DEBUG then debugFix doc lineStr constant ] @@ -1421,7 +1413,6 @@ module private IntFix = CommonFixes.replaceWithNamedConstantFix doc pos - lineStr parseAndCheck constant.Constant constant.Range @@ -1432,7 +1423,6 @@ module private IntFix = CommonFixes.replaceWithNamedConstantFix doc pos - lineStr parseAndCheck constant.Constant constant.Range @@ -1598,7 +1588,7 @@ let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = asyncResult { let filePath = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath let fcsPos = protocolPosToPos codeActionParams.Range.Start - let! (parseAndCheck, lineStr, sourceText) = getParseResultsForFile filePath fcsPos + let! (parseAndCheck, lineStr, _sourceText) = getParseResultsForFile filePath fcsPos match tryFindConstant parseAndCheck.GetAST fcsPos with | None -> return [] @@ -1658,7 +1648,7 @@ let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = let constant = CharConstant.parse (lineStr, range, constant, char value) CharFix.all doc lineStr error constant | IntConstant constant -> IntFix.all doc fcsPos lineStr parseAndCheck error constant - | SynConst.UserNum(_, _) -> + | SynConst.UserNum _ -> let constant = IntConstant.parse (lineStr, range, constant) IntFix.all doc fcsPos lineStr parseAndCheck error constant | SynConst.Single _ diff --git a/src/FsAutoComplete/CodeFixes/ChangeDerefBangToValue.fs b/src/FsAutoComplete/CodeFixes/ChangeDerefBangToValue.fs index 2762e1c66..efc654224 100644 --- a/src/FsAutoComplete/CodeFixes/ChangeDerefBangToValue.fs +++ b/src/FsAutoComplete/CodeFixes/ChangeDerefBangToValue.fs @@ -30,7 +30,7 @@ let private tryGetRangeOfDeref input derefPos = let title = "Use `.Value` instead of dereference operator" -let fix (getParseResultsForFile: GetParseResultsForFile) (getLineText: GetLineText) : CodeFix = +let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = Run.ifDiagnosticByCode (Set.ofList [ "3370" ]) (fun diagnostic codeActionParams -> asyncResult { let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath diff --git a/src/FsAutoComplete/CodeFixes/ChangeDerefBangToValue.fsi b/src/FsAutoComplete/CodeFixes/ChangeDerefBangToValue.fsi index a557339e1..3f59cd8e6 100644 --- a/src/FsAutoComplete/CodeFixes/ChangeDerefBangToValue.fsi +++ b/src/FsAutoComplete/CodeFixes/ChangeDerefBangToValue.fsi @@ -13,5 +13,4 @@ val title: string val fix: getParseResultsForFile: GetParseResultsForFile -> - getLineText: GetLineText -> (Ionide.LanguageServerProtocol.Types.CodeActionParams -> Async>) diff --git a/src/FsAutoComplete/CodeFixes/ChangeRefCellDerefToNot.fs b/src/FsAutoComplete/CodeFixes/ChangeRefCellDerefToNot.fs index 631eeecf2..304b813d0 100644 --- a/src/FsAutoComplete/CodeFixes/ChangeRefCellDerefToNot.fs +++ b/src/FsAutoComplete/CodeFixes/ChangeRefCellDerefToNot.fs @@ -15,7 +15,7 @@ let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath let fcsPos = protocolPosToPos diagnostic.Range.Start - let! (tyRes, line, lines) = getParseResultsForFile fileName fcsPos + let! tyRes, _line, _lines = getParseResultsForFile fileName fcsPos match tyRes.GetParseResults.TryRangeOfRefCellDereferenceContainingPos fcsPos with | Some derefRange -> diff --git a/src/FsAutoComplete/CodeFixes/ChangeTypeOfNameToNameOf.fs b/src/FsAutoComplete/CodeFixes/ChangeTypeOfNameToNameOf.fs index 53056f0c0..c70b84b8c 100644 --- a/src/FsAutoComplete/CodeFixes/ChangeTypeOfNameToNameOf.fs +++ b/src/FsAutoComplete/CodeFixes/ChangeTypeOfNameToNameOf.fs @@ -45,7 +45,7 @@ let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = let pos = protocolPosToPos codeActionParams.Range.Start - let! (tyRes, line, sourceText) = getParseResultsForFile fileName pos + let! tyRes, _line, sourceText = getParseResultsForFile fileName pos let! results = tyRes.GetParseResults.TryRangeOfTypeofWithNameAndTypeExpr(pos) diff --git a/src/FsAutoComplete/CodeFixes/ConvertCSharpLambdaToFSharpLambda.fs b/src/FsAutoComplete/CodeFixes/ConvertCSharpLambdaToFSharpLambda.fs index b99c35ffe..cfa3221e0 100644 --- a/src/FsAutoComplete/CodeFixes/ConvertCSharpLambdaToFSharpLambda.fs +++ b/src/FsAutoComplete/CodeFixes/ConvertCSharpLambdaToFSharpLambda.fs @@ -19,13 +19,12 @@ let private tryRangeOfParenEnclosingOpEqualsGreaterUsage input pos = let (|InfixAppOfOpEqualsGreater|_|) = function - | SynExpr.App(ExprAtomicFlag.NonAtomic, - false, - SynExpr.App(ExprAtomicFlag.NonAtomic, true, Ident "op_EqualsGreater", actualParamListExpr, range), - actualLambdaBodyExpr, - _) -> + | SynExpr.App( + flag = ExprAtomicFlag.NonAtomic + isInfix = false + funcExpr = SynExpr.App(ExprAtomicFlag.NonAtomic, true, Ident "op_EqualsGreater", actualParamListExpr, range)) -> let opEnd = range.End - let opStart = Position.mkPos (range.End.Line) (range.End.Column - 2) + let opStart = Position.mkPos range.End.Line (range.End.Column - 2) let opRange = Range.mkRange range.FileName opStart opEnd let argsRange = actualParamListExpr.Range @@ -49,7 +48,7 @@ let private tryRangeOfParenEnclosingOpEqualsGreaterUsage input pos = | _ -> defaultTraverse binding } ) -let fix (getParseResultsForFile: GetParseResultsForFile) (getLineText: GetLineText) : CodeFix = +let fix (getParseResultsForFile: GetParseResultsForFile) (_: GetLineText) : CodeFix = Run.ifDiagnosticByCode (Set.ofList [ "39" // undefined value @@ -59,7 +58,7 @@ let fix (getParseResultsForFile: GetParseResultsForFile) (getLineText: GetLineTe let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath let fcsPos = protocolPosToPos diagnostic.Range.Start - let! (tyRes, _, lines) = getParseResultsForFile fileName fcsPos + let! tyRes, _, _ = getParseResultsForFile fileName fcsPos match tryRangeOfParenEnclosingOpEqualsGreaterUsage tyRes.GetAST fcsPos with | Some(argsRange, opRange) -> diff --git a/src/FsAutoComplete/CodeFixes/ConvertInvalidRecordToAnonRecord.fs b/src/FsAutoComplete/CodeFixes/ConvertInvalidRecordToAnonRecord.fs index 5f9aee0cb..ee9b95d58 100644 --- a/src/FsAutoComplete/CodeFixes/ConvertInvalidRecordToAnonRecord.fs +++ b/src/FsAutoComplete/CodeFixes/ConvertInvalidRecordToAnonRecord.fs @@ -16,7 +16,7 @@ let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath let fcsPos = protocolPosToPos diagnostic.Range.Start - let! (tyRes, line, lines) = getParseResultsForFile fileName fcsPos + let! tyRes, _line, lines = getParseResultsForFile fileName fcsPos match tyRes.GetParseResults.TryRangeOfRecordExpressionContainingPos fcsPos with | Some recordExpressionRange -> diff --git a/src/FsAutoComplete/CodeFixes/ConvertPositionalDUToNamed.fs b/src/FsAutoComplete/CodeFixes/ConvertPositionalDUToNamed.fs index 5239f34c1..2341b0b07 100644 --- a/src/FsAutoComplete/CodeFixes/ConvertPositionalDUToNamed.fs +++ b/src/FsAutoComplete/CodeFixes/ConvertPositionalDUToNamed.fs @@ -124,15 +124,15 @@ let private toPosSeq (range: FSharp.Compiler.Text.Range, text: IFSACSourceText) let title = "Convert to named patterns" -let fix (getParseResultsForFile: GetParseResultsForFile) (getRangeText: GetRangeText) : CodeFix = +let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = fun codeActionParams -> asyncResult { let filePath = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath let fcsPos = protocolPosToPos codeActionParams.Range.Start - let! (parseAndCheck, lineStr, sourceText) = getParseResultsForFile filePath fcsPos + let! parseAndCheck, lineStr, sourceText = getParseResultsForFile filePath fcsPos - let! (duIdent, duFields, parenRange) = + let! duIdent, duFields, parenRange = parseAndCheck.TryGetPositionalUnionPattern(fcsPos) |> Result.ofOption (fun _ -> "Not inside a DU pattern") diff --git a/src/FsAutoComplete/CodeFixes/ConvertPositionalDUToNamed.fsi b/src/FsAutoComplete/CodeFixes/ConvertPositionalDUToNamed.fsi index 6c1cb4e40..8313fa995 100644 --- a/src/FsAutoComplete/CodeFixes/ConvertPositionalDUToNamed.fsi +++ b/src/FsAutoComplete/CodeFixes/ConvertPositionalDUToNamed.fsi @@ -25,7 +25,4 @@ open FsAutoComplete.LspHelpers val title: string -val fix: - getParseResultsForFile: GetParseResultsForFile -> - getRangeText: GetRangeText -> - (CodeActionParams -> Async>) +val fix: getParseResultsForFile: GetParseResultsForFile -> (CodeActionParams -> Async>) diff --git a/src/FsAutoComplete/CodeFixes/ConvertTripleSlashCommentToXmlTaggedDoc.fs b/src/FsAutoComplete/CodeFixes/ConvertTripleSlashCommentToXmlTaggedDoc.fs index 74aeaa0ce..2bb0f4dee 100644 --- a/src/FsAutoComplete/CodeFixes/ConvertTripleSlashCommentToXmlTaggedDoc.fs +++ b/src/FsAutoComplete/CodeFixes/ConvertTripleSlashCommentToXmlTaggedDoc.fs @@ -134,7 +134,7 @@ let private collectCommentContents match currentLine with | None -> acc | Some line -> - let idx = line.IndexOf("///") + let idx = line.IndexOf("///", StringComparison.Ordinal) if idx >= 0 then let existingComment = line.TrimStart().Substring(3).TrimStart() @@ -162,7 +162,7 @@ let private wrapInSummary indent comments = } |> String.concat "" -let fix (getParseResultsForFile: GetParseResultsForFile) (getRangeText: GetRangeText) : CodeFix = +let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = fun codeActionParams -> asyncResult { let filePath = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath @@ -177,7 +177,7 @@ let fix (getParseResultsForFile: GetParseResultsForFile) (getRangeText: GetRange let origCommentContents = collectCommentContents d.Range.Start d.Range.End sourceText - let indent = lineStr.IndexOf("///") + let indent = lineStr.IndexOf("///", StringComparison.Ordinal) let summaryXmlDoc = wrapInSummary indent origCommentContents let range = diff --git a/src/FsAutoComplete/CodeFixes/ConvertTripleSlashCommentToXmlTaggedDoc.fsi b/src/FsAutoComplete/CodeFixes/ConvertTripleSlashCommentToXmlTaggedDoc.fsi index 1344fee8d..29e81b3c5 100644 --- a/src/FsAutoComplete/CodeFixes/ConvertTripleSlashCommentToXmlTaggedDoc.fsi +++ b/src/FsAutoComplete/CodeFixes/ConvertTripleSlashCommentToXmlTaggedDoc.fsi @@ -12,7 +12,4 @@ open System val title: string -val fix: - getParseResultsForFile: GetParseResultsForFile -> - getRangeText: GetRangeText -> - (CodeActionParams -> Async>) +val fix: getParseResultsForFile: GetParseResultsForFile -> (CodeActionParams -> Async>) diff --git a/src/FsAutoComplete/CodeFixes/GenerateRecordStub.fs b/src/FsAutoComplete/CodeFixes/GenerateRecordStub.fs index 81d28915a..8f70aec75 100644 --- a/src/FsAutoComplete/CodeFixes/GenerateRecordStub.fs +++ b/src/FsAutoComplete/CodeFixes/GenerateRecordStub.fs @@ -11,7 +11,7 @@ let title = "Generate record stub" /// a codefix that generates member stubs for a record declaration let fix (getParseResultsForFile: GetParseResultsForFile) - (genRecordStub: _ -> _ -> _ -> _ -> Async>) + (genRecordStub: _ -> _ -> _ -> Async>) (getTextReplacements: unit -> Map) : CodeFix = fun codeActionParams -> @@ -20,9 +20,9 @@ let fix let pos = protocolPosToPos codeActionParams.Range.Start - let! (tyRes, line, lines) = getParseResultsForFile fileName pos + let! tyRes, _line, lines = getParseResultsForFile fileName pos - match! genRecordStub tyRes pos lines line with + match! genRecordStub tyRes pos lines with | CoreResponse.Res(text, position) -> let replacements = getTextReplacements () diff --git a/src/FsAutoComplete/CodeFixes/GenerateRecordStub.fsi b/src/FsAutoComplete/CodeFixes/GenerateRecordStub.fsi index a7395e255..c37529c99 100644 --- a/src/FsAutoComplete/CodeFixes/GenerateRecordStub.fsi +++ b/src/FsAutoComplete/CodeFixes/GenerateRecordStub.fsi @@ -11,6 +11,6 @@ val title: string /// a codefix that generates member stubs for a record declaration val fix: getParseResultsForFile: GetParseResultsForFile -> - genRecordStub: (ParseAndCheckResults -> FcsPos -> IFSACSourceText -> string -> Async>) -> + genRecordStub: (ParseAndCheckResults -> FcsPos -> IFSACSourceText -> Async>) -> getTextReplacements: (unit -> Map) -> (CodeActionParams -> Async>) diff --git a/src/FsAutoComplete/CodeFixes/GenerateUnionCases.fs b/src/FsAutoComplete/CodeFixes/GenerateUnionCases.fs index f4707a5db..60f75d947 100644 --- a/src/FsAutoComplete/CodeFixes/GenerateUnionCases.fs +++ b/src/FsAutoComplete/CodeFixes/GenerateUnionCases.fs @@ -14,7 +14,7 @@ let title = "Generate union pattern match cases" let fix (getFileLines: GetFileLines) (getParseResultsForFile: GetParseResultsForFile) - (generateCases: _ -> _ -> _ -> _ -> Async>) + (generateCases: _ -> _ -> _ -> Async>) (getTextReplacements: unit -> Map) = Run.ifDiagnosticByCode (Set.ofList [ "25" ]) (fun diagnostic codeActionParams -> @@ -37,9 +37,9 @@ let fix let casePosFCS = protocolPosToPos casePos - let! (tyRes, line, lines) = getParseResultsForFile fileName casePosFCS + let! tyRes, _line, lines = getParseResultsForFile fileName casePosFCS - match! generateCases tyRes casePosFCS lines line |> Async.map Ok with + match! generateCases tyRes casePosFCS lines |> Async.map Ok with | CoreResponse.Res(insertString: string, insertPosition) -> let range = { Start = fcsPosToLsp insertPosition diff --git a/src/FsAutoComplete/CodeFixes/GenerateUnionCases.fsi b/src/FsAutoComplete/CodeFixes/GenerateUnionCases.fsi index d9f19f22e..d1340b670 100644 --- a/src/FsAutoComplete/CodeFixes/GenerateUnionCases.fsi +++ b/src/FsAutoComplete/CodeFixes/GenerateUnionCases.fsi @@ -14,6 +14,6 @@ val title: string val fix: getFileLines: GetFileLines -> getParseResultsForFile: GetParseResultsForFile -> - generateCases: (ParseAndCheckResults -> FcsPos -> IFSACSourceText -> string -> Async>) -> + generateCases: (ParseAndCheckResults -> FcsPos -> IFSACSourceText -> Async>) -> getTextReplacements: (unit -> Map) -> (CodeActionParams -> Async>) diff --git a/src/FsAutoComplete/CodeFixes/ImplementInterface.fs b/src/FsAutoComplete/CodeFixes/ImplementInterface.fs index cf0c33b1d..37832168e 100644 --- a/src/FsAutoComplete/CodeFixes/ImplementInterface.fs +++ b/src/FsAutoComplete/CodeFixes/ImplementInterface.fs @@ -146,12 +146,7 @@ let private tryFindInsertionData (interfaceData: InterfaceData) (ast: ParsedInpu | InterfaceData.ObjExpr(_, bindings) -> bindings |> List.tryLast match lastExistingMember with - | Some(SynBinding( - attributes = attributes - valData = SynValData(memberFlags = memberFlags) - headPat = headPat - expr = expr - trivia = trivia)) -> + | Some(SynBinding(attributes = attributes; valData = SynValData _; headPat = headPat; expr = expr; trivia = trivia)) -> // align with existing member // insert after last member @@ -234,11 +229,7 @@ let titleWithTypeAnnotation = "Implement interface" let titleWithoutTypeAnnotation = "Implement interface without type annotation" /// codefix that generates members for an interface implementation -let fix - (getParseResultsForFile: GetParseResultsForFile) - (getProjectOptionsForFile: GetProjectOptionsForFile) - (config: unit -> Config) - : CodeFix = +let fix (getParseResultsForFile: GetParseResultsForFile) (config: unit -> Config) : CodeFix = Run.ifDiagnosticByCode (Set.ofList [ "366" ]) (fun diagnostic codeActionParams -> asyncResult { // diagnostic range: @@ -250,7 +241,7 @@ let fix let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath let startPos = protocolPosToPos codeActionParams.Range.Start - let! (tyRes, line, lines) = getParseResultsForFile fileName startPos + let! tyRes, line, lines = getParseResultsForFile fileName startPos let! interfaceData = InterfaceStubGenerator.TryFindInterfaceDeclaration startPos tyRes.GetAST diff --git a/src/FsAutoComplete/CodeFixes/ImplementInterface.fsi b/src/FsAutoComplete/CodeFixes/ImplementInterface.fsi index edf350807..de3b2bb18 100644 --- a/src/FsAutoComplete/CodeFixes/ImplementInterface.fsi +++ b/src/FsAutoComplete/CodeFixes/ImplementInterface.fsi @@ -22,6 +22,5 @@ val titleWithoutTypeAnnotation: string /// codefix that generates members for an interface implementation val fix: getParseResultsForFile: GetParseResultsForFile -> - getProjectOptionsForFile: GetProjectOptionsForFile -> config: (unit -> Config) -> (CodeActionParams -> Async>) diff --git a/src/FsAutoComplete/CodeFixes/MakeDeclarationMutable.fs b/src/FsAutoComplete/CodeFixes/MakeDeclarationMutable.fs index e2ff534ff..ff2e2ac36 100644 --- a/src/FsAutoComplete/CodeFixes/MakeDeclarationMutable.fs +++ b/src/FsAutoComplete/CodeFixes/MakeDeclarationMutable.fs @@ -19,11 +19,11 @@ let fix let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath let fcsPos = protocolPosToPos diagnostic.Range.Start - let! (tyRes, line, lines) = getParseResultsForFile fileName fcsPos + let! tyRes, line, _lines = getParseResultsForFile fileName fcsPos let! opts = getProjectOptionsForFile fileName match Lexer.getSymbol fcsPos.Line fcsPos.Column line SymbolLookupKind.Fuzzy opts.OtherOptions with - | Some symbol -> + | Some _symbol -> match! tyRes.TryFindDeclaration fcsPos line with | FindDeclarationResult.Range declRange when declRange.FileName = (UMX.untag fileName) -> let lspRange = fcsRangeToLsp declRange diff --git a/src/FsAutoComplete/CodeFixes/RemoveRedundantAttributeSuffix.fs b/src/FsAutoComplete/CodeFixes/RemoveRedundantAttributeSuffix.fs index 1326e179f..16f371e5e 100644 --- a/src/FsAutoComplete/CodeFixes/RemoveRedundantAttributeSuffix.fs +++ b/src/FsAutoComplete/CodeFixes/RemoveRedundantAttributeSuffix.fs @@ -1,5 +1,6 @@ module FsAutoComplete.CodeFix.RemoveRedundantAttributeSuffix +open System open FSharp.Compiler.Syntax open FsToolkit.ErrorHandling open FsAutoComplete.CodeFix.Types @@ -26,7 +27,7 @@ let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = attributes.Attributes |> List.choose (fun a -> match List.tryLast a.TypeName.LongIdent with - | Some ident when ident.idText.EndsWith("Attribute") -> Some ident + | Some ident when ident.idText.EndsWith("Attribute", StringComparison.Ordinal) -> Some ident | _ -> None) if List.isEmpty attributesWithRedundantSuffix then diff --git a/src/FsAutoComplete/CodeFixes/RemoveUnnecessaryReturnOrYield.fs b/src/FsAutoComplete/CodeFixes/RemoveUnnecessaryReturnOrYield.fs index f96bb8dcc..bce61d06c 100644 --- a/src/FsAutoComplete/CodeFixes/RemoveUnnecessaryReturnOrYield.fs +++ b/src/FsAutoComplete/CodeFixes/RemoveUnnecessaryReturnOrYield.fs @@ -1,5 +1,6 @@ module FsAutoComplete.CodeFix.RemoveUnnecessaryReturnOrYield +open System open FsToolkit.ErrorHandling open FsAutoComplete.CodeFix.Types open Ionide.LanguageServerProtocol.Types @@ -15,7 +16,7 @@ let fix (getParseResultsForFile: GetParseResultsForFile) (getLineText: GetLineTe let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath let fcsPos = protocolPosToPos diagnostic.Range.Start - let! (tyRes, line, lines) = getParseResultsForFile fileName fcsPos + let! tyRes, _line, lines = getParseResultsForFile fileName fcsPos let fcsErrorPos = protocolPosToPos diagnostic.Range.Start match tyRes.GetParseResults.TryRangeOfExprInYieldOrReturn fcsErrorPos with @@ -26,13 +27,13 @@ let fix (getParseResultsForFile: GetParseResultsForFile) (getLineText: GetLineTe let! errorText = getLineText lines diagnostic.Range let! title = - if errorText.StartsWith "return!" then + if errorText.StartsWith("return!", StringComparison.Ordinal) then Ok(title "return!") - elif errorText.StartsWith "yield!" then + elif errorText.StartsWith("yield!", StringComparison.Ordinal) then Ok(title "yield!") - elif errorText.StartsWith "return" then + elif errorText.StartsWith("return", StringComparison.Ordinal) then Ok(title "return") - elif errorText.StartsWith "yield" then + elif errorText.StartsWith("yield", StringComparison.Ordinal) then Ok(title "yield") else Error "unknown start token for remove return or yield codefix" diff --git a/src/FsAutoComplete/CodeFixes/RemoveUnusedBinding.fs b/src/FsAutoComplete/CodeFixes/RemoveUnusedBinding.fs index e49101efb..db6bd4d4a 100644 --- a/src/FsAutoComplete/CodeFixes/RemoveUnusedBinding.fs +++ b/src/FsAutoComplete/CodeFixes/RemoveUnusedBinding.fs @@ -1,6 +1,6 @@ module FsAutoComplete.CodeFix.RemoveUnusedBinding - +open System open FsToolkit.ErrorHandling open FsAutoComplete.CodeFix.Navigation open FsAutoComplete.CodeFix.Types @@ -12,10 +12,6 @@ open FSharp.Compiler.Syntax open FSharp.Compiler.Text -let posBetween (range: Range) tester = - Position.posGeq tester range.Start // positions on this one are flipped to simulate Pos.posLte, because that doesn't exist - && Position.posGeq range.End tester - type private ReplacementRangeResult = | FullBinding of bindingRange: Range | Pattern of patternRange: Range @@ -62,7 +58,9 @@ type FSharpParseFileResults with else // Check if it's an operator match pat with - | SynPat.LongIdent(longDotId = SynLongIdent(id = [ id ])) when id.idText.StartsWith("op_") -> + | SynPat.LongIdent(longDotId = SynLongIdent(id = [ id ])) when + id.idText.StartsWith("op_", StringComparison.Ordinal) + -> if Range.rangeContainsRange id.idRange diagnosticRange then Some(FullBinding binding.RangeOfBindingWithRhs) else @@ -83,7 +81,7 @@ let fix (getParseResults: GetParseResultsForFile) : CodeFix = let fcsRange = protocolRangeToRange (codeActionParams.TextDocument.GetFilePath()) diagnostic.Range - let! tyres, line, lines = getParseResults fileName fcsRange.Start + let! tyres, _line, lines = getParseResults fileName fcsRange.Start let! rangeOfBinding = tyres.GetParseResults.TryRangeOfBindingWithHeadPatternWithPos(fcsRange) diff --git a/src/FsAutoComplete/CodeFixes/RenameParamToMatchSignature.fs b/src/FsAutoComplete/CodeFixes/RenameParamToMatchSignature.fs index 1a724702e..2b6ccaf74 100644 --- a/src/FsAutoComplete/CodeFixes/RenameParamToMatchSignature.fs +++ b/src/FsAutoComplete/CodeFixes/RenameParamToMatchSignature.fs @@ -1,5 +1,6 @@ module FsAutoComplete.CodeFix.RenameParamToMatchSignature +open System open FsToolkit.ErrorHandling open FsAutoComplete.CodeFix.Types open Ionide.LanguageServerProtocol.Types @@ -27,8 +28,8 @@ let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = let head = "The argument names in the signature '" let mid = "' and implementation '" - if msg.StartsWith head then - match msg.IndexOf mid with + if msg.StartsWith(head, StringComparison.Ordinal) then + match msg.IndexOf(mid, StringComparison.Ordinal) with | -1 -> None | i -> msg.Substring(head.Length, i - head.Length) |> Some else diff --git a/src/FsAutoComplete/CodeFixes/ReplaceWithSuggestion.fs b/src/FsAutoComplete/CodeFixes/ReplaceWithSuggestion.fs index afe8615e8..e1ec606bc 100644 --- a/src/FsAutoComplete/CodeFixes/ReplaceWithSuggestion.fs +++ b/src/FsAutoComplete/CodeFixes/ReplaceWithSuggestion.fs @@ -8,10 +8,27 @@ open FSharp.Compiler.Syntax let title suggestion = $"Replace with '%s{suggestion}'" +let undefinedName = + [ "Maybe you want one of the following:" + "Možná budete potřebovat něco z tohoto:" + "Vielleicht möchten Sie eine der folgenden Bezeichnungen verwenden:" + "Puede elegir una de las opciones siguientes:" + "Peut-être souhaitez-vous l'une des options suivantes :" + "Si può specificare uno dei nomi seguenti:" + "次のいずれかの可能性はありませんか:" + "다음 중 하나가 필요할 수 있습니다:" + "Możliwe, że chcesz wykonać jedną z następujących czynności:" + "Talvez você queira um dos seguintes:" + "Возможно, требуется одно из следующих:" + "Aşağıdakilerden birini arıyor olabilirsiniz:" + "你可能需要以下之一:" + "您可能需要下列其中一項:" ] + |> List.map (fun i -> fun (j: string) -> j.Contains(i, System.StringComparison.Ordinal)) + /// a codefix that replaces the use of an unknown identifier with a suggested identifier let fix = - Run.ifDiagnosticByMessage "Maybe you want one of the following:" (fun diagnostic codeActionParams -> - diagnostic.Message.Split('\n').[1..] + Run.ifDiagnosticByCheckMessage undefinedName (fun diagnostic codeActionParams -> + diagnostic.Message.Split('\n', '\u001b').[1..] |> Array.map (fun suggestion -> let suggestion = suggestion.Trim() |> PrettyNaming.NormalizeIdentifierBackticks diff --git a/src/FsAutoComplete/CodeFixes/ResolveNamespace.fs b/src/FsAutoComplete/CodeFixes/ResolveNamespace.fs index f8b59b266..a47b28620 100644 --- a/src/FsAutoComplete/CodeFixes/ResolveNamespace.fs +++ b/src/FsAutoComplete/CodeFixes/ResolveNamespace.fs @@ -1,5 +1,6 @@ module FsAutoComplete.CodeFix.ResolveNamespace +open System open Ionide.LanguageServerProtocol.Types open FsAutoComplete.CodeFix open FsAutoComplete.CodeFix.Types @@ -8,9 +9,29 @@ open FsAutoComplete.LspHelpers open FsAutoComplete open FSharp.Compiler.Text open FSharp.Compiler.EditorServices +open System.Text.RegularExpressions type LineText = string +let undefinedName = + [ "not define" + "nedefinuje|Není definovaný|Není definované|Není definovaná|Nemáte definovaný" + "definiert nicht|nicht.*? definiert" + "no define|no está definido|no está definida" + "ne définit|n'est pas défini" + "non definisce|non è definito|non è definita" + "定義(され|し)ていません" + "정의(하지 않|되지 않았|되어 있지 않)습니다" + "nie definiuje|Nie zdefiniowano|nie jest zdefiniowany" + "não define|não está definido" + "не определяет|не определено|не определены|не определен" + "tanımlamıyor|tanımlı değil" + "未.*?定义" + "未定義" ] + |> List.map (fun i -> + let regex = Regex(i, RegexOptions.IgnoreCase ||| RegexOptions.Compiled) + fun (j: string) -> regex.IsMatch(j)) + /// a codefix the provides suggestions for opening modules or using qualified names when an identifier is found that needs qualification let fix (getParseResultsForFile: GetParseResultsForFile) @@ -37,7 +58,10 @@ let fix let line = lines.GetLineString(l - 2) let isImplicitTopLevelModule = - not (line.StartsWith "module" && not (line.EndsWith "=")) + not ( + line.StartsWith("module", StringComparison.Ordinal) + && not (line.EndsWith("=", StringComparison.Ordinal)) + ) if isImplicitTopLevelModule then 1 else l | ScopeKind.TopModule -> 1 @@ -47,7 +71,11 @@ let fix lineNos |> List.mapi (fun i line -> i, lines.GetLineString line) - |> List.choose (fun (i, lineStr) -> if lineStr.StartsWith "namespace" then Some i else None) + |> List.choose (fun (i, lineStr) -> + if lineStr.StartsWith("namespace", StringComparison.Ordinal) then + Some i + else + None) |> List.tryLast match mostRecentNamespaceInScope with @@ -73,12 +101,12 @@ let fix Title = $"Use %s{qual}" Kind = FixKind.Fix } - let openFix (text: ISourceText) file diagnostic (word: string) (ns, name: string, ctx, multiple) : Fix = + let openFix (text: ISourceText) file diagnostic (word: string) (ns, name: string, ctx, _multiple) : Fix = let insertPoint = adjustInsertionPoint text ctx let docLine = insertPoint - 1 let actualOpen = - if name.EndsWith word && name <> word then + if name.EndsWith(word, StringComparison.Ordinal) && name <> word then let prefix = name.Substring(0, name.Length - word.Length).TrimEnd('.') $"%s{ns}.%s{prefix}" @@ -112,7 +140,7 @@ let fix Title = $"open %s{actualOpen}" Kind = FixKind.Fix } - Run.ifDiagnosticByMessage "is not defined" (fun diagnostic codeActionParameter -> + Run.ifDiagnosticByCheckMessage undefinedName (fun diagnostic codeActionParameter -> asyncResult { let pos = protocolPosToPos diagnostic.Range.Start @@ -121,8 +149,8 @@ let fix let! tyRes, line, lines = getParseResultsForFile filePath pos match! getNamespaceSuggestions tyRes pos line with - | CoreResponse.InfoRes msg - | CoreResponse.ErrorRes msg -> return [] + | CoreResponse.InfoRes _msg + | CoreResponse.ErrorRes _msg -> return [] | CoreResponse.Res(word, opens, qualifiers) -> let quals = qualifiers diff --git a/src/FsAutoComplete/CodeFixes/ToInterpolatedString.fs b/src/FsAutoComplete/CodeFixes/ToInterpolatedString.fs index c68a89a9f..b834e6a49 100644 --- a/src/FsAutoComplete/CodeFixes/ToInterpolatedString.fs +++ b/src/FsAutoComplete/CodeFixes/ToInterpolatedString.fs @@ -1,5 +1,6 @@ module FsAutoComplete.CodeFix.ToInterpolatedString +open System open System.Text.RegularExpressions open FsToolkit.ErrorHandling open FsAutoComplete.CodeFix.Types @@ -87,7 +88,9 @@ let tryFindSprintfApplication (parseAndCheck: ParseAndCheckResults) (sourceText: parseAndCheck.TryGetSymbolUse functionIdent.idRange.End lineStr |> Option.bind (fun symbolUse -> match symbolUse.Symbol with - | :? FSharpMemberOrFunctionOrValue as mfv when mfv.Assembly.QualifiedName.StartsWith("FSharp.Core") -> + | :? FSharpMemberOrFunctionOrValue as mfv when + mfv.Assembly.QualifiedName.StartsWith("FSharp.Core", StringComparison.Ordinal) + -> // Verify the function is from F# Core. Some(functionIdent, mString, xs, mLastArg) | _ -> None)) diff --git a/src/FsAutoComplete/CodeFixes/ToInterpolatedString.fsi b/src/FsAutoComplete/CodeFixes/ToInterpolatedString.fsi new file mode 100644 index 000000000..5adc16bfc --- /dev/null +++ b/src/FsAutoComplete/CodeFixes/ToInterpolatedString.fsi @@ -0,0 +1,12 @@ +module FsAutoComplete.CodeFix.ToInterpolatedString + +open FsAutoComplete.CodeFix.Types +open Ionide.LanguageServerProtocol.Types + +val title: string + +val fix: + getParseResultsForFile: GetParseResultsForFile -> + getLanguageVersion: GetLanguageVersion -> + codeActionParams: CodeActionParams -> + Async> diff --git a/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs b/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs new file mode 100644 index 000000000..f3cddb442 --- /dev/null +++ b/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fs @@ -0,0 +1,102 @@ +module FsAutoComplete.CodeFix.UpdateValueInSignatureFile + +open FSharp.Compiler.Diagnostics +open FSharp.Compiler.Diagnostics.ExtendedData +open FSharp.Compiler.Syntax +open FSharp.Compiler.Text +open FsToolkit.ErrorHandling +open Ionide.LanguageServerProtocol.Types +open FsAutoComplete.CodeFix.Types +open FsAutoComplete +open FsAutoComplete.LspHelpers + +#nowarn "57" + +let title = "Update val in signature file" + +let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = + Run.ifDiagnosticByCode (Set.ofList [ "34" ]) (fun diagnostic codeActionParams -> + asyncResult { + let implFilePath = codeActionParams.TextDocument.GetFilePath() + let sigFilePath = $"%s{implFilePath}i" + + let implFileName = Utils.normalizePath implFilePath + let sigFileName = Utils.normalizePath sigFilePath + + let sigTextDocumentIdentifier: TextDocumentIdentifier = + { Uri = $"%s{codeActionParams.TextDocument.Uri}i" } + + let! (implParseAndCheckResults: ParseAndCheckResults, implLine: string, _implSourceText: IFSACSourceText) = + getParseResultsForFile implFileName (protocolPosToPos diagnostic.Range.Start) + + let mDiag = + protocolRangeToRange implParseAndCheckResults.GetParseResults.FileName diagnostic.Range + + let! extendedDiagnosticData = + implParseAndCheckResults.GetCheckResults.Diagnostics + |> Array.tryPick (fun (diag: FSharpDiagnostic) -> + if diag.ErrorNumber <> 34 || not (Range.equals diag.Range mDiag) then + None + else + match diag.ExtendedData with + | Some(:? ValueNotContainedDiagnosticExtendedData as extendedData) -> Some extendedData + | _ -> None) + |> Result.ofOption (fun () -> "No extended data") + + // Find the binding name in the implementation file. + let impVisitor = + { new SyntaxVisitorBase<_>() with + override x.VisitBinding(path, defaultTraverse, SynBinding(headPat = pat)) = + match pat with + | SynPat.LongIdent(longDotId = SynLongIdent(id = [ ident ])) when Range.equals mDiag ident.idRange -> + Some ident + | _ -> None } + + let! implBindingIdent = + SyntaxTraversal.Traverse(mDiag.Start, implParseAndCheckResults.GetParseResults.ParseTree, impVisitor) + |> Result.ofOption (fun () -> "No binding name found") + + let endPos = implBindingIdent.idRange.End + + let! symbolUse = + implParseAndCheckResults.GetCheckResults.GetSymbolUseAtLocation( + endPos.Line, + endPos.Column, + implLine, + [ implBindingIdent.idText ] + ) + |> Result.ofOption (fun () -> "No symbolUse found") + + let! valText = + extendedDiagnosticData.ImplementationValue.GetValSignatureText(symbolUse.DisplayContext, symbolUse.Range) + |> Result.ofOption (fun () -> "No val text found.") + + // Find a matching val in the signature file. + let sigVisitor = + { new SyntaxVisitorBase<_>() with + override x.VisitValSig(path, defaultTraverse, SynValSig(range = mValSig)) = + if Range.rangeContainsRange mValSig extendedDiagnosticData.SignatureValue.DeclarationLocation then + Some mValSig + else + None } + + let! (sigParseAndCheckResults: ParseAndCheckResults, _sigLine: string, _sigSourceText: IFSACSourceText) = + getParseResultsForFile sigFileName extendedDiagnosticData.SignatureValue.DeclarationLocation.End + + let! mVal = + SyntaxTraversal.Traverse( + extendedDiagnosticData.SignatureValue.DeclarationLocation.End, + sigParseAndCheckResults.GetParseResults.ParseTree, + sigVisitor + ) + |> Result.ofOption (fun () -> "No val range found in signature file") + + return + [ { SourceDiagnostic = None + Title = title + File = sigTextDocumentIdentifier + Edits = + [| { Range = fcsRangeToLsp mVal + NewText = valText } |] + Kind = FixKind.Fix } ] + }) diff --git a/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fsi b/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fsi new file mode 100644 index 000000000..ede271327 --- /dev/null +++ b/src/FsAutoComplete/CodeFixes/UpdateValueInSignatureFile.fsi @@ -0,0 +1,6 @@ +module FsAutoComplete.CodeFix.UpdateValueInSignatureFile + +open FsAutoComplete.CodeFix.Types + +val title: string +val fix: getParseResultsForFile: GetParseResultsForFile -> CodeFix diff --git a/src/FsAutoComplete/CodeFixes/UseMutationWhenValueIsMutable.fs b/src/FsAutoComplete/CodeFixes/UseMutationWhenValueIsMutable.fs index 9f1f0b53f..6e295f3b3 100644 --- a/src/FsAutoComplete/CodeFixes/UseMutationWhenValueIsMutable.fs +++ b/src/FsAutoComplete/CodeFixes/UseMutationWhenValueIsMutable.fs @@ -17,7 +17,7 @@ let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath let fcsPos = protocolPosToPos diagnostic.Range.Start - let! (tyRes, line, lines) = getParseResultsForFile fileName fcsPos + let! tyRes, _line, lines = getParseResultsForFile fileName fcsPos match walkForwardUntilCondition lines diagnostic.Range.Start System.Char.IsWhiteSpace with | None -> return [] diff --git a/src/FsAutoComplete/CodeFixes/UseTripleQuotedInterpolation.fs b/src/FsAutoComplete/CodeFixes/UseTripleQuotedInterpolation.fs index cccc67af3..85ed3428a 100644 --- a/src/FsAutoComplete/CodeFixes/UseTripleQuotedInterpolation.fs +++ b/src/FsAutoComplete/CodeFixes/UseTripleQuotedInterpolation.fs @@ -10,7 +10,7 @@ open FsAutoComplete.FCSPatches let title = "Use triple-quoted string interpolation" /// a codefix that replaces erroring single-quoted interpolations with triple-quoted interpolations -let fix (getParseResultsForFile: GetParseResultsForFile) (getRangeText: GetRangeText) : CodeFix = +let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = Run.ifDiagnosticByCode (Set.ofList [ "3373" ]) (fun diagnostic codeActionParams -> asyncResult { let pos = protocolPosToPos diagnostic.Range.Start diff --git a/src/FsAutoComplete/CodeFixes/UseTripleQuotedInterpolation.fsi b/src/FsAutoComplete/CodeFixes/UseTripleQuotedInterpolation.fsi index 7bcbc5276..cac8bc0cb 100644 --- a/src/FsAutoComplete/CodeFixes/UseTripleQuotedInterpolation.fsi +++ b/src/FsAutoComplete/CodeFixes/UseTripleQuotedInterpolation.fsi @@ -10,7 +10,4 @@ open FsAutoComplete.FCSPatches val title: string /// a codefix that replaces erroring single-quoted interpolations with triple-quoted interpolations -val fix: - getParseResultsForFile: GetParseResultsForFile -> - getRangeText: GetRangeText -> - (CodeActionParams -> Async>) +val fix: getParseResultsForFile: GetParseResultsForFile -> (CodeActionParams -> Async>) diff --git a/src/FsAutoComplete/CodeFixes/WrapExpressionInParentheses.fs b/src/FsAutoComplete/CodeFixes/WrapExpressionInParentheses.fs index 4f1debe8f..13d3cb497 100644 --- a/src/FsAutoComplete/CodeFixes/WrapExpressionInParentheses.fs +++ b/src/FsAutoComplete/CodeFixes/WrapExpressionInParentheses.fs @@ -8,7 +8,7 @@ open FsAutoComplete let title = "Wrap expression in parentheses" /// a codefix that parenthesizes a member expression that needs it -let fix (getRangeText: GetRangeText) : CodeFix = +let fix: CodeFix = Run.ifDiagnosticByCode (Set.ofList [ "597" ]) (fun diagnostic codeActionParams -> AsyncResult.retn [ { Title = title diff --git a/src/FsAutoComplete/CodeFixes/WrapExpressionInParentheses.fsi b/src/FsAutoComplete/CodeFixes/WrapExpressionInParentheses.fsi index 8b93959cd..4416d5eaf 100644 --- a/src/FsAutoComplete/CodeFixes/WrapExpressionInParentheses.fsi +++ b/src/FsAutoComplete/CodeFixes/WrapExpressionInParentheses.fsi @@ -7,4 +7,4 @@ open FsAutoComplete val title: string /// a codefix that parenthesizes a member expression that needs it -val fix: getRangeText: GetRangeText -> (CodeActionParams -> Async>) +val fix: (CodeActionParams -> Async>) diff --git a/src/FsAutoComplete/CommandResponse.fs b/src/FsAutoComplete/CommandResponse.fs index 592fe4a7d..6d54bfeee 100644 --- a/src/FsAutoComplete/CommandResponse.fs +++ b/src/FsAutoComplete/CommandResponse.fs @@ -91,7 +91,7 @@ module internal ClassificationUtils = | SemanticClassificationType.Value | SemanticClassificationType.LocalValue -> "variable" | SemanticClassificationType.Plaintext -> "text" - | n -> "unknown" + | _n -> "unknown" module CommandResponse = open FSharp.Compiler.Text @@ -211,7 +211,7 @@ module CommandResponse = static member IsIgnored(e: FSharpDiagnostic) = // FST-1027 support in Fake 5 - e.ErrorNumber = 213 && e.Message.StartsWith "'paket:" + e.ErrorNumber = 213 && e.Message.StartsWith("'paket:", StringComparison.Ordinal) static member OfFSharpError(e: FSharpDiagnostic) = { FileName = e.FileName @@ -460,7 +460,7 @@ module CommandResponse = match x.Kind with | Ionide.ProjInfo.InspectSln.SolutionItemKind.Unknown | Ionide.ProjInfo.InspectSln.SolutionItemKind.Unsupported -> None - | Ionide.ProjInfo.InspectSln.SolutionItemKind.MsbuildFormat msbuildProj -> + | Ionide.ProjInfo.InspectSln.SolutionItemKind.MsbuildFormat _ -> Some( WorkspacePeekFoundSolutionItemKind.MsbuildFormat { WorkspacePeekFoundSolutionItemKindMsbuildFormat.Configurations = [] } diff --git a/src/FsAutoComplete/FsAutoComplete.fsproj b/src/FsAutoComplete/FsAutoComplete.fsproj index 014cc81c9..fbc7f3054 100644 --- a/src/FsAutoComplete/FsAutoComplete.fsproj +++ b/src/FsAutoComplete/FsAutoComplete.fsproj @@ -29,12 +29,14 @@ + + diff --git a/src/FsAutoComplete/JsonSerializer.fs b/src/FsAutoComplete/JsonSerializer.fs index ecba41b6c..110369bb3 100644 --- a/src/FsAutoComplete/JsonSerializer.fs +++ b/src/FsAutoComplete/JsonSerializer.fs @@ -23,7 +23,7 @@ module private JsonSerializerConverters = serializer.Serialize(writer, value) - override x.ReadJson(reader, t, existingValue, serializer) = + override x.ReadJson(reader, t, _existingValue, serializer) = let innerType = t.GetGenericArguments().[0] let innerType = diff --git a/src/FsAutoComplete/LspHelpers.fs b/src/FsAutoComplete/LspHelpers.fs index 02e15d63c..c72b6b6de 100644 --- a/src/FsAutoComplete/LspHelpers.fs +++ b/src/FsAutoComplete/LspHelpers.fs @@ -146,14 +146,17 @@ module Conversions = let applyQuery (query: string) (info: SymbolInformation) = match query.Split([| '.' |], StringSplitOptions.RemoveEmptyEntries) with | [||] -> false - | [| fullName |] -> info.Name.StartsWith fullName - | [| moduleName; fieldName |] -> info.Name.StartsWith fieldName && info.ContainerName = Some moduleName + | [| fullName |] -> info.Name.StartsWith(fullName, StringComparison.Ordinal) + | [| moduleName; fieldName |] -> + info.Name.StartsWith(fieldName, StringComparison.Ordinal) + && info.ContainerName = Some moduleName | parts -> let containerName = parts.[0 .. (parts.Length - 2)] |> String.concat "." let fieldName = Array.last parts - info.Name.StartsWith fieldName && info.ContainerName = Some containerName + info.Name.StartsWith(fieldName, StringComparison.Ordinal) + && info.ContainerName = Some containerName let getCodeLensInformation (uri: DocumentUri) (typ: string) (topLevel: NavigationTopLevelDeclaration) : CodeLens[] = let map (decl: NavigationItem) : CodeLens = @@ -302,7 +305,7 @@ module Workspace = match x.Kind with | Ionide.ProjInfo.InspectSln.SolutionItemKind.Unknown | Ionide.ProjInfo.InspectSln.SolutionItemKind.Unsupported -> None - | Ionide.ProjInfo.InspectSln.SolutionItemKind.MsbuildFormat msbuildProj -> + | Ionide.ProjInfo.InspectSln.SolutionItemKind.MsbuildFormat _msbuildProj -> Some( WorkspacePeekFoundSolutionItemKind.MsbuildFormat { WorkspacePeekFoundSolutionItemKindMsbuildFormat.Configurations = [] } @@ -356,7 +359,7 @@ module SignatureData = let args = parms - |> List.map (fun group -> group |> List.map (fun (n, t) -> formatType t) |> String.concat " * ") + |> List.map (fun group -> group |> List.map (snd >> formatType) |> String.concat " * ") |> String.concat " -> " if String.IsNullOrEmpty args then @@ -528,7 +531,7 @@ module ClassificationUtils = | SemanticClassificationType.Value | SemanticClassificationType.LocalValue -> SemanticTokenTypes.Variable, [] | SemanticClassificationType.Plaintext -> SemanticTokenTypes.Text, [] - | unknown -> SemanticTokenTypes.Text, [] + | _unknown -> SemanticTokenTypes.Text, [] type PlainNotification = { Content: string } @@ -633,7 +636,7 @@ type FSACDto = type FSharpConfigDto = { AutomaticWorkspaceInit: bool option WorkspaceModePeekDeepLevel: int option - ExcludeProjectDirectories: string[] option + ExcludeProjectDirectories: string array option KeywordsAutocomplete: bool option ExternalAutocomplete: bool option FullNameExternalAutocomplete: bool option @@ -657,13 +660,15 @@ type FSharpConfigDto = ResolveNamespaces: bool option EnableReferenceCodeLens: bool option EnableAnalyzers: bool option - AnalyzersPath: string[] option + AnalyzersPath: string array option + ExcludeAnalyzers: string array option + IncludeAnalyzers: string array option DisableInMemoryProjectReferences: bool option LineLens: LineLensConfig option UseSdkScripts: bool option DotNetRoot: string option - FSIExtraParameters: string[] option - FSICompilerToolLocations: string[] option + FSIExtraParameters: string array option + FSICompilerToolLocations: string array option TooltipMode: string option GenerateBinlog: bool option AbstractClassStubGeneration: bool option @@ -769,7 +774,7 @@ let tryCreateRegex (pattern: string) = type FSharpConfig = { AutomaticWorkspaceInit: bool WorkspaceModePeekDeepLevel: int - ExcludeProjectDirectories: string[] + ExcludeProjectDirectories: string array KeywordsAutocomplete: bool ExternalAutocomplete: bool FullNameExternalAutocomplete: bool @@ -796,13 +801,15 @@ type FSharpConfig = ResolveNamespaces: bool EnableReferenceCodeLens: bool EnableAnalyzers: bool - AnalyzersPath: string[] + AnalyzersPath: string array + ExcludeAnalyzers: string array + IncludeAnalyzers: string array DisableInMemoryProjectReferences: bool LineLens: LineLensConfig UseSdkScripts: bool DotNetRoot: string - FSIExtraParameters: string[] - FSICompilerToolLocations: string[] + FSIExtraParameters: string array + FSICompilerToolLocations: string array TooltipMode: string GenerateBinlog: bool CodeLenses: CodeLensConfig @@ -843,6 +850,8 @@ type FSharpConfig = EnableReferenceCodeLens = false EnableAnalyzers = false AnalyzersPath = [||] + ExcludeAnalyzers = [||] + IncludeAnalyzers = [||] DisableInMemoryProjectReferences = false LineLens = { Enabled = "never"; Prefix = "" } UseSdkScripts = true @@ -864,7 +873,7 @@ type FSharpConfig = ExcludeProjectDirectories = defaultArg dto.ExcludeProjectDirectories [||] KeywordsAutocomplete = defaultArg dto.KeywordsAutocomplete false ExternalAutocomplete = defaultArg dto.ExternalAutocomplete false - FullNameExternalAutocomplete = defaultArg dto.ExternalAutocomplete false + FullNameExternalAutocomplete = defaultArg dto.FullNameExternalAutocomplete false IndentationSize = defaultArg dto.IndentationSize 4 Linter = defaultArg dto.Linter false LinterConfig = dto.LinterConfig @@ -891,6 +900,8 @@ type FSharpConfig = EnableReferenceCodeLens = defaultArg dto.EnableReferenceCodeLens false EnableAnalyzers = defaultArg dto.EnableAnalyzers false AnalyzersPath = defaultArg dto.AnalyzersPath [||] + ExcludeAnalyzers = defaultArg dto.ExcludeAnalyzers [||] + IncludeAnalyzers = defaultArg dto.IncludeAnalyzers [||] DisableInMemoryProjectReferences = defaultArg dto.DisableInMemoryProjectReferences false LineLens = { Enabled = defaultArg (dto.LineLens |> Option.map (fun n -> n.Enabled)) "never" @@ -996,6 +1007,8 @@ type FSharpConfig = EnableReferenceCodeLens = defaultArg dto.EnableReferenceCodeLens x.EnableReferenceCodeLens EnableAnalyzers = defaultArg dto.EnableAnalyzers x.EnableAnalyzers AnalyzersPath = defaultArg dto.AnalyzersPath x.AnalyzersPath + ExcludeAnalyzers = defaultArg dto.ExcludeAnalyzers x.ExcludeAnalyzers + IncludeAnalyzers = defaultArg dto.IncludeAnalyzers x.IncludeAnalyzers DisableInMemoryProjectReferences = defaultArg dto.DisableInMemoryProjectReferences x.DisableInMemoryProjectReferences LineLens = diff --git a/src/FsAutoComplete/LspHelpers.fsi b/src/FsAutoComplete/LspHelpers.fsi index adeceb83b..e6bd689ab 100644 --- a/src/FsAutoComplete/LspHelpers.fsi +++ b/src/FsAutoComplete/LspHelpers.fsi @@ -266,7 +266,7 @@ type FSACDto = type FSharpConfigDto = { AutomaticWorkspaceInit: bool option WorkspaceModePeekDeepLevel: int option - ExcludeProjectDirectories: string[] option + ExcludeProjectDirectories: string array option KeywordsAutocomplete: bool option ExternalAutocomplete: bool option FullNameExternalAutocomplete: bool option @@ -290,13 +290,15 @@ type FSharpConfigDto = ResolveNamespaces: bool option EnableReferenceCodeLens: bool option EnableAnalyzers: bool option - AnalyzersPath: string[] option + AnalyzersPath: string array option + ExcludeAnalyzers: string array option + IncludeAnalyzers: string array option DisableInMemoryProjectReferences: bool option LineLens: LineLensConfig option UseSdkScripts: bool option DotNetRoot: string option - FSIExtraParameters: string[] option - FSICompilerToolLocations: string[] option + FSIExtraParameters: string array option + FSICompilerToolLocations: string array option TooltipMode: string option GenerateBinlog: bool option AbstractClassStubGeneration: bool option @@ -361,7 +363,7 @@ type DebugConfig = type FSharpConfig = { AutomaticWorkspaceInit: bool WorkspaceModePeekDeepLevel: int - ExcludeProjectDirectories: string[] + ExcludeProjectDirectories: string array KeywordsAutocomplete: bool ExternalAutocomplete: bool FullNameExternalAutocomplete: bool @@ -388,13 +390,15 @@ type FSharpConfig = ResolveNamespaces: bool EnableReferenceCodeLens: bool EnableAnalyzers: bool - AnalyzersPath: string[] + AnalyzersPath: string array + ExcludeAnalyzers: string array + IncludeAnalyzers: string array DisableInMemoryProjectReferences: bool LineLens: LineLensConfig UseSdkScripts: bool DotNetRoot: string - FSIExtraParameters: string[] - FSICompilerToolLocations: string[] + FSIExtraParameters: string array + FSICompilerToolLocations: string array TooltipMode: string GenerateBinlog: bool CodeLenses: CodeLensConfig diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index ebba809ec..130161c96 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -540,9 +540,9 @@ type AdaptiveFSharpLspServer return None // An empty file has empty completions. Otherwise we would error down there else - let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.ofStringErr + let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.lineLookupErr - if lineStr.StartsWith "#" then + if lineStr.StartsWith("#", StringComparison.Ordinal) then let completionList = { IsIncomplete = false Items = KeywordList.hashSymbolCompletionItems @@ -568,7 +568,12 @@ type AdaptiveFSharpLspServer asyncResult { let! volatileFile = state.GetOpenFileOrRead filePath - let! lineStr = volatileFile.Source |> tryGetLineStr pos + + let! lineStr = + volatileFile.Source + |> tryGetLineStr pos + |> Result.mapError ErrorMsgUtils.formatLineLookErr + //TODO ⮝⮝⮝ good candidate for better error model -- review! // TextDocumentCompletion will sometimes come in before TextDocumentDidChange // This will require the trigger character to be at the place VSCode says it is @@ -701,7 +706,7 @@ type AdaptiveFSharpLspServer { ci with Detail = Some symbolName Documentation = Some d } - | HelpText.Full(name, tip, additionalEdit) -> + | HelpText.Full(_name, tip, additionalEdit) -> let (si, comment) = TipFormatter.formatCompletionItemTip tip let edits, label = @@ -737,7 +742,10 @@ type AdaptiveFSharpLspServer | true, s -> CoreResponse.Res(HelpText.Simple(sym, s)) | _ -> let sym = - if sym.StartsWith "``" && sym.EndsWith "``" then + if + sym.StartsWith("``", StringComparison.Ordinal) + && sym.EndsWith("``", StringComparison.Ordinal) + then sym.TrimStart([| '`' |]).TrimEnd([| '`' |]) else sym @@ -745,7 +753,7 @@ type AdaptiveFSharpLspServer match state.GetAutoCompleteByDeclName sym with | None -> //Isn't in sync filled cache, we don't have result CoreResponse.ErrorRes(sprintf "No help text available for symbol '%s'" sym) - | Some(decl, pos, fn, _, _) -> //Is in sync filled cache, try to get results from async filled caches or calculate if it's not there + | Some(decl, _pos, _fn, _, _) -> //Is in sync filled cache, try to get results from async filled caches or calculate if it's not there let tip = decl.Description @@ -871,11 +879,11 @@ type AdaptiveFSharpLspServer let (filePath, pos) = getFilePathAndPosition p let! volatileFile = state.GetOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.ofStringErr + let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.lineLookupErr and! tyRes = state.GetOpenFileTypeCheckResultsCached filePath |> AsyncResult.ofStringErr match tyRes.TryGetToolTipEnhanced pos lineStr with - | Ok(Some tooltipResult) -> + | Some tooltipResult -> logger.info ( Log.setMessage "TryGetToolTipEnhanced : {params}" >> Log.addContextDestructured "params" tooltipResult @@ -936,13 +944,8 @@ type AdaptiveFSharpLspServer | TipFormatter.TipFormatterResult.None -> return None - | Ok(None) -> + | None -> return None - return! LspResult.internalError $"No TryGetToolTipEnhanced results for {filePath}" - | Error e -> - trace.RecordError(e, "TextDocumentHover.Error") |> ignore - logger.error (Log.setMessage "Failed with {error}" >> Log.addContext "error" e) - return! LspResult.internalError e with e -> trace |> Tracing.recordException e @@ -964,7 +967,7 @@ type AdaptiveFSharpLspServer let (filePath, pos) = getFilePathAndPosition p let! volatileFile = state.GetOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.ofStringErr + let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.lineLookupErr let! tyRes = state.GetOpenFileTypeCheckResults filePath |> AsyncResult.ofStringErr let! (_, _, range) = @@ -987,12 +990,12 @@ type AdaptiveFSharpLspServer let (filePath, pos) = getFilePathAndPosition p let! volatileFile = state.GetOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.ofStringErr + let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.lineLookupErr and! tyRes = state.GetOpenFileTypeCheckResults filePath |> AsyncResult.ofStringErr // validate name and surround with backticks if necessary let! newName = - Commands.adjustRenameSymbolNewName pos lineStr volatileFile.Source tyRes p.NewName + Commands.adjustRenameSymbolNewName pos lineStr tyRes p.NewName |> AsyncResult.mapError (fun msg -> JsonRpc.Error.Create(JsonRpc.ErrorCodes.invalidParams, msg)) // safety check: rename valid? @@ -1061,7 +1064,7 @@ type AdaptiveFSharpLspServer let (filePath, pos) = getFilePathAndPosition p let! volatileFile = state.GetOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.ofStringErr + let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.lineLookupErr and! tyRes = state.GetOpenFileTypeCheckResults filePath |> AsyncResult.ofStringErr let! decl = tyRes.TryFindDeclaration pos lineStr |> AsyncResult.ofStringErr return decl |> findDeclToLspLocation |> GotoResult.Single |> Some @@ -1091,7 +1094,7 @@ type AdaptiveFSharpLspServer let (filePath, pos) = getFilePathAndPosition p let! volatileFile = state.GetOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.ofStringErr + let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.lineLookupErr and! tyRes = state.GetOpenFileTypeCheckResults filePath |> AsyncResult.ofStringErr let! decl = tyRes.TryFindTypeDeclaration pos lineStr |> AsyncResult.ofStringErr return decl |> findDeclToLspLocation |> GotoResult.Single |> Some @@ -1120,7 +1123,7 @@ type AdaptiveFSharpLspServer let (filePath, pos) = getFilePathAndPosition p let! volatileFile = state.GetOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = tryGetLineStr pos volatileFile.Source |> Result.ofStringErr + let! lineStr = tryGetLineStr pos volatileFile.Source |> Result.lineLookupErr and! tyRes = state.GetOpenFileTypeCheckResults filePath |> AsyncResult.ofStringErr let! usages = @@ -1156,16 +1159,16 @@ type AdaptiveFSharpLspServer let (filePath, pos) = getFilePathAndPosition p let! volatileFile = state.GetOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = tryGetLineStr pos volatileFile.Source |> Result.ofStringErr + let! lineStr = tryGetLineStr pos volatileFile.Source |> Result.lineLookupErr and! tyRes = state.GetOpenFileTypeCheckResults filePath |> AsyncResult.ofStringErr match tyRes.TryGetSymbolUseAndUsages pos lineStr |> Result.bimap CoreResponse.Res CoreResponse.InfoRes with - | CoreResponse.InfoRes msg -> return None + | CoreResponse.InfoRes _msg -> return None | CoreResponse.ErrorRes msg -> return! LspResult.internalError msg - | CoreResponse.Res(symbol, uses) -> + | CoreResponse.Res(_symbol, uses) -> return uses |> Array.map (fun s -> @@ -1198,7 +1201,7 @@ type AdaptiveFSharpLspServer let (filePath, pos) = getFilePathAndPosition p let! volatileFile = state.GetOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = tryGetLineStr pos volatileFile.Source |> Result.ofStringErr + let! lineStr = tryGetLineStr pos volatileFile.Source |> Result.lineLookupErr and! tyRes = state.GetOpenFileTypeCheckResults filePath |> AsyncResult.ofStringErr logger.info ( @@ -1265,7 +1268,7 @@ type AdaptiveFSharpLspServer return decls |> Array.collect (fun top -> - getSymbolInformations p.TextDocument.Uri state.GlyphToSymbolKind top (fun s -> true)) + getSymbolInformations p.TextDocument.Uri state.GlyphToSymbolKind top (fun _s -> true)) |> U2.First |> Some | None -> return! LspResult.internalError $"No declarations for {fn}" @@ -1391,7 +1394,7 @@ type AdaptiveFSharpLspServer Commands.formatSelection tryGetFileCheckerOptionsWithLines formatSelectionAsync fileName range - let handlerFormattedRangeDoc (sourceText: IFSACSourceText, formatted: string, range: FormatSelectionRange) = + let handlerFormattedRangeDoc (_sourceText: IFSACSourceText, formatted: string, range: FormatSelectionRange) = let range = { Start = { Line = range.StartLine - 1 @@ -1557,7 +1560,7 @@ type AdaptiveFSharpLspServer ) let! (sourceText: IFSACSourceText) = state.GetOpenFileSource filePath |> AsyncResult.ofStringErr - let! lineStr = sourceText |> tryGetLineStr pos |> Result.ofStringErr + let! lineStr = sourceText |> tryGetLineStr pos |> Result.lineLookupErr let typ = data.[1] let! r = Async.Catch(f arg pos tyRes sourceText lineStr typ filePath) @@ -1989,7 +1992,7 @@ type AdaptiveFSharpLspServer let! tyRes = state.GetOpenFileTypeCheckResults filePath |> AsyncResult.ofStringErr - let fcsRange = protocolRangeToRange (UMX.untag filePath) p.Range + let _fcsRange = protocolRangeToRange (UMX.untag filePath) p.Range let! pipelineHints = Commands.inlineValues volatileFile.Source tyRes @@ -2068,7 +2071,7 @@ type AdaptiveFSharpLspServer let filePath = Path.FileUriToLocalPath p.Item.Uri |> Utils.normalizePath let pos = protocolPosToPos p.Item.SelectionRange.Start let! volatileFile = state.GetOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = tryGetLineStr pos volatileFile.Source |> Result.ofStringErr + let! lineStr = tryGetLineStr pos volatileFile.Source |> Result.lineLookupErr // Incoming file may not be "Opened" so we need to force a typecheck let! tyRes = state.GetTypeCheckResultsForFile filePath |> AsyncResult.ofStringErr @@ -2155,7 +2158,7 @@ type AdaptiveFSharpLspServer |> getFilePathAndPosition let! volatileFile = state.GetOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = tryGetLineStr pos volatileFile.Source |> Result.ofStringErr + let! lineStr = tryGetLineStr pos volatileFile.Source |> Result.lineLookupErr and! tyRes = state.GetOpenFileTypeCheckResults filePath |> AsyncResult.ofStringErr let! decl = tyRes.TryFindDeclaration pos lineStr |> AsyncResult.ofStringErr @@ -2237,9 +2240,9 @@ type AdaptiveFSharpLspServer let (filePath, pos) = getFilePathAndPosition p let! volatileFile = state.GetOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.ofStringErr + let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.lineLookupErr and! tyRes = state.GetOpenFileTypeCheckResults filePath |> AsyncResult.ofStringErr - let! tip = Commands.typesig tyRes pos lineStr |> Result.ofCoreResponse + let tip = Commands.typesig tyRes pos lineStr return tip @@ -2272,7 +2275,7 @@ type AdaptiveFSharpLspServer let filePath = p.TextDocument.GetFilePath() |> Utils.normalizePath let! volatileFile = state.GetOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.ofStringErr + let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.lineLookupErr and! tyRes = state.GetOpenFileTypeCheckResults filePath |> AsyncResult.ofStringErr let! (typ, parms, generics) = tyRes.TryGetSignatureData pos lineStr |> Result.ofStringErr @@ -2308,7 +2311,7 @@ type AdaptiveFSharpLspServer let (filePath, pos) = getFilePathAndPosition p let! volatileFile = state.GetOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.ofStringErr + let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.lineLookupErr and! tyRes = state.GetOpenFileTypeCheckResults filePath |> AsyncResult.ofStringErr match! @@ -2656,7 +2659,7 @@ type AdaptiveFSharpLspServer let (filePath, pos) = getFilePathAndPosition p let! volatileFile = state.GetOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.ofStringErr + let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.lineLookupErr and! tyRes = state.GetOpenFileTypeCheckResults filePath |> AsyncResult.ofStringErr match! Commands.Help tyRes pos lineStr |> Result.ofCoreResponse with @@ -2687,7 +2690,7 @@ type AdaptiveFSharpLspServer let (filePath, pos) = getFilePathAndPosition p let! volatileFile = state.GetOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.ofStringErr + let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.lineLookupErr and! tyRes = state.GetOpenFileTypeCheckResults filePath |> AsyncResult.ofStringErr lastFSharpDocumentationTypeCheck <- Some tyRes @@ -3084,7 +3087,7 @@ type AdaptiveFSharpLspServer } member this.CallHierarchyOutgoingCalls - (arg1: CallHierarchyOutgoingCallsParams) + (_arg1: CallHierarchyOutgoingCallsParams) : AsyncLspResult = AsyncLspResult.notImplemented @@ -3106,7 +3109,7 @@ module AdaptiveFSharpLspServer = None | _ -> None - let strategy = StreamJsonRpcTracingStrategy(Tracing.fsacActivitySource) + let _strategy = StreamJsonRpcTracingStrategy(Tracing.fsacActivitySource) let (|Flatten|_|) (e: exn) = match e with diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index a7af4ec7b..fc84e778d 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -127,6 +127,11 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac [| yield! fsiCompilerToolLocations |> Array.map toCompilerToolArgument yield! fsiExtraParameters |] + let analyzersClient = + FSharp.Analyzers.SDK.Client( + Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance + ) + /// Loads F# Analyzers from the configured directories /// The FSharpConfig /// The RootPath @@ -135,6 +140,18 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac if config.EnableAnalyzers then Loggers.analyzers.info (Log.setMessageI $"Using analyzer roots of {config.AnalyzersPath:roots}") + let excludeInclude = + match config.ExcludeAnalyzers, config.IncludeAnalyzers with + | e, [||] -> FSharp.Analyzers.SDK.ExcludeInclude.ExcludeFilter(fun (s: string) -> Array.contains s e) + | [||], i -> FSharp.Analyzers.SDK.ExcludeInclude.IncludeFilter(fun (s: string) -> Array.contains s i) + | _e, i -> + Loggers.analyzers.warn ( + Log.setMessage + "--exclude-analyzers and --include-analyzers are mutually exclusive, ignoring --exclude-analyzers" + ) + + FSharp.Analyzers.SDK.ExcludeInclude.IncludeFilter(fun (s: string) -> Array.contains s i) + config.AnalyzersPath |> Array.iter (fun analyzerPath -> match rootPath with @@ -152,7 +169,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac Loggers.analyzers.info (Log.setMessageI $"Loading analyzers from {dir:dir}") - let (dllCount, analyzerCount) = dir |> FSharp.Analyzers.SDK.Client.loadAnalyzers + let (dllCount, analyzerCount) = analyzersClient.LoadAnalyzers(dir, excludeInclude) Loggers.analyzers.info ( Log.setMessageI @@ -369,14 +386,14 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac // Since analyzers are not async, we need to switch to a new thread to not block threadpool do! Async.SwitchToNewThread() - let res = + let! res = Commands.analyzerHandler ( + analyzersClient, file, - volatileFile.Source.ToString().Split("\n"), - parseAndCheck.GetParseResults.ParseTree, + volatileFile.Source, + parseAndCheck.GetParseResults, tast, - parseAndCheck.GetCheckResults.PartialAssemblySignature.Entities |> Seq.toList, - parseAndCheck.GetAllEntities + parseAndCheck.GetCheckResults ) let! ct = Async.CancellationToken @@ -428,7 +445,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac CommandResponse.projectLoading JsonSerializer.writeJson projectFileName | ProjectResponse.WorkspaceLoad(finished) -> CommandResponse.workspaceLoad JsonSerializer.writeJson finished - | ProjectResponse.ProjectChanged(projectFileName) -> failwith "Not Implemented" + | ProjectResponse.ProjectChanged(_projectFileName) -> failwith "Not Implemented" logger.info (Log.setMessage "Workspace Notify {ws}" >> Log.addContextDestructured "ws" ws) do! ({ Content = ws }: PlainNotification) |> lspClient.NotifyWorkspace @@ -552,6 +569,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let severity = match m.Severity with + | FSharp.Analyzers.SDK.Hint -> DiagnosticSeverity.Hint | FSharp.Analyzers.SDK.Info -> DiagnosticSeverity.Information | FSharp.Analyzers.SDK.Warning -> DiagnosticSeverity.Warning | FSharp.Analyzers.SDK.Error -> DiagnosticSeverity.Error @@ -604,7 +622,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac () } |> fun work -> Async.StartImmediate(work, ct) - with :? OperationCanceledException as e -> + with :? OperationCanceledException -> () @@ -689,7 +707,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac |> ASet.mapAtoAMap (UMX.untag >> AdaptiveFile.GetLastWriteTimeUtc) let cb = - projChanges.AddCallback(fun old delta -> + projChanges.AddCallback(fun _old delta -> logger.info ( Log.setMessage "Loading projects because of {delta}" >> Log.addContextDestructured "delta" delta @@ -777,7 +795,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac | MSBuildAllProjects v -> yield! v - |> Array.filter (fun x -> x.EndsWith(".props") && isWithinObjFolder x) + |> Array.filter (fun x -> x.EndsWith(".props", StringComparison.Ordinal) && isWithinObjFolder x) |> Array.map (UMX.tag >> projectFileChanges) | _ -> () ] @@ -985,13 +1003,10 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac // ignore if already cancelled () - - let cachedFileContents = cmap, asyncaval> () - let resetCancellationToken filePath = let adder _ = new CancellationTokenSource() - let updater key value = + let updater _key value = cancelToken filePath value new CancellationTokenSource() @@ -1143,7 +1158,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let allFSharpFilesAndProjectOptions = let wins = openFilesToChangesAndProjectOptions - |> AMap.map (fun k v -> v |> AsyncAVal.mapSync (fun (file, projects) _ -> Some file, projects)) + |> AMap.map (fun _k v -> v |> AsyncAVal.mapSync (fun (file, projects) _ -> Some file, projects)) let loses = sourceFileToProjectOptions @@ -1157,11 +1172,11 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let allFilesToFSharpProjectOptions = allFSharpFilesAndProjectOptions - |> AMapAsync.mapAsyncAVal (fun filePath (file, options) ctok -> AsyncAVal.constant options) + |> AMapAsync.mapAsyncAVal (fun _filePath (_file, options) _ctok -> AsyncAVal.constant options) let allFilesParsed = allFSharpFilesAndProjectOptions - |> AMapAsync.mapAsyncAVal (fun filePath (file, options: LoadedProject list) ctok -> + |> AMapAsync.mapAsyncAVal (fun _filePath (file, options: LoadedProject list) _ctok -> asyncAVal { let! (checker: FSharpCompilerServiceChecker) = checker @@ -1234,7 +1249,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let autoCompleteNamespaces = autoCompleteItems - |> AMap.choose (fun name (d, pos, fn, getLine, ast) -> + |> AMap.choose (fun _name (d, pos, _fn, getLine, ast) -> Commands.calculateNamespaceInsert (fun () -> Some ast) d pos getLine) @@ -1347,7 +1362,6 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return! Error(e.ToString()) } - let openFilesToRecentCheckedFilesResults = openFilesToChangesAndProjectOptions |> AMapAsync.mapAsyncAVal (fun _ (info, projectOptions) _ -> @@ -1407,6 +1421,35 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac return results |> Result.ofOption (fun () -> $"No typecheck results for {filePath}") } + + let forceGetProjectOptions filePath = + asyncAVal { + let! projects = getProjectOptionsForFile filePath + let project = selectProject projects + + return + project + |> Result.ofOption (fun () -> $"Could not find project containing {filePath}") + + } + |> AsyncAVal.forceAsync + + let forceGetFSharpProjectOptions filePath = + forceGetProjectOptions filePath + |> Async.map (Result.map (fun p -> p.FSharpProjectOptions)) + + + let forceGetOpenFileTypeCheckResultsOrCheck file = + async { + match! forceGetOpenFileTypeCheckResults file with + | Ok x -> return Ok x + | Error _ -> + match! forceGetFSharpProjectOptions file with + | Ok opts -> return! bypassAdaptiveTypeCheck file opts + | Error e -> return Error e + } + + /// /// This will attempt to get typecheck results in this order /// @@ -1439,7 +1482,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac do! forceGetOpenFileTypeCheckResults filePath |> Async.Ignore> - with e -> + with _e -> () } ) @@ -1450,7 +1493,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let allFilesToDeclarations = allFilesParsed - |> AMap.map (fun k v -> v |> AsyncAVal.mapOption (fun p _ -> p.GetNavigationItems().Declarations)) + |> AMap.map (fun _k v -> v |> AsyncAVal.mapOption (fun p _ -> p.GetNavigationItems().Declarations)) let getAllDeclarations () = async { @@ -1472,21 +1515,6 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let getDeclarations filename = allFilesToDeclarations |> AMapAsync.tryFindAndFlatten filename - let forceGetProjectOptions filePath = - asyncAVal { - let! projects = getProjectOptionsForFile filePath - let project = selectProject projects - - return - project - |> Result.ofOption (fun () -> $"Could not find project containing {filePath}") - - } - |> AsyncAVal.forceAsync - - let forceGetFSharpProjectOptions filePath = - forceGetProjectOptions filePath - |> Async.map (Result.map (fun p -> p.FSharpProjectOptions)) let codeGenServer = { new ICodeGenerationService with @@ -1631,11 +1659,16 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let codefixes = - let tryGetParseResultsForFile filePath pos = + let tryGetParseAndCheckResultsForFile filePath pos = asyncResult { let! (file) = forceFindOpenFileOrRead filePath - let! lineStr = file.Source |> tryGetLineStr pos - and! tyRes = forceGetOpenFileTypeCheckResults filePath + + let! lineStr = + file.Source + |> tryGetLineStr pos + |> Result.mapError ErrorMsgUtils.formatLineLookErr + //TODO ⮝⮝⮝ good candidate for better error model -- review! + and! tyRes = forceGetOpenFileTypeCheckResultsOrCheck filePath return tyRes, lineStr, file.Source } @@ -1647,8 +1680,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let tryFindUnionDefinitionFromPos = tryFindUnionDefinitionFromPos codeGenServer - let getUnionPatternMatchCases tyRes pos sourceText line = - Commands.getUnionPatternMatchCases tryFindUnionDefinitionFromPos tyRes pos sourceText line + let getUnionPatternMatchCases tyRes pos sourceText = + Commands.getUnionPatternMatchCases tryFindUnionDefinitionFromPos tyRes pos sourceText let unionCaseStubReplacements (config) () = Map.ofList [ "$1", config.UnionCaseStubGenerationBody ] @@ -1663,8 +1696,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac let tryFindRecordDefinitionFromPos = RecordStubGenerator.tryFindRecordDefinitionFromPos codeGenServer - let getRecordStub tyRes pos sourceText line = - Commands.getRecordStub (tryFindRecordDefinitionFromPos) tyRes pos sourceText line + let getRecordStub tyRes pos sourceText = + Commands.getRecordStub (tryFindRecordDefinitionFromPos) tyRes pos sourceText let getLineText (sourceText: IFSACSourceText) (range: Ionide.LanguageServerProtocol.Types.Range) = sourceText.GetText(protocolRangeToRange (UMX.untag sourceText.FileName) range) @@ -1706,70 +1739,70 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac [| Run.ifEnabled (fun _ -> config.UnusedOpensAnalyzer) (RemoveUnusedOpens.fix forceFindSourceText) Run.ifEnabled (fun _ -> config.ResolveNamespaces) - (ResolveNamespace.fix tryGetParseResultsForFile Commands.getNamespaceSuggestions) + (ResolveNamespace.fix tryGetParseAndCheckResultsForFile Commands.getNamespaceSuggestions) ReplaceWithSuggestion.fix RemoveRedundantQualifier.fix - Run.ifEnabled (fun _ -> config.UnusedDeclarationsAnalyzer) (RenameUnusedValue.fix tryGetParseResultsForFile) - AddNewKeywordToDisposableConstructorInvocation.fix getRangeText + Run.ifEnabled + (fun _ -> config.UnusedDeclarationsAnalyzer) + (RenameUnusedValue.fix tryGetParseAndCheckResultsForFile) + AddNewKeywordToDisposableConstructorInvocation.fix Run.ifEnabled (fun _ -> config.UnionCaseStubGeneration) (GenerateUnionCases.fix forceFindSourceText - tryGetParseResultsForFile + tryGetParseAndCheckResultsForFile getUnionPatternMatchCases (unionCaseStubReplacements config)) ExternalSystemDiagnostics.linter ExternalSystemDiagnostics.analyzers Run.ifEnabled (fun _ -> config.InterfaceStubGeneration) - (ImplementInterface.fix - tryGetParseResultsForFile - forceGetFSharpProjectOptions - (implementInterfaceConfig config)) + (ImplementInterface.fix tryGetParseAndCheckResultsForFile (implementInterfaceConfig config)) Run.ifEnabled (fun _ -> config.RecordStubGeneration) - (GenerateRecordStub.fix tryGetParseResultsForFile getRecordStub (recordStubReplacements config)) + (GenerateRecordStub.fix tryGetParseAndCheckResultsForFile getRecordStub (recordStubReplacements config)) Run.ifEnabled (fun _ -> config.AbstractClassStubGeneration) (GenerateAbstractClassStub.fix - tryGetParseResultsForFile + tryGetParseAndCheckResultsForFile getAbstractClassStub (abstractClassStubReplacements config)) AddMissingEqualsToTypeDefinition.fix forceFindSourceText ChangePrefixNegationToInfixSubtraction.fix forceFindSourceText ConvertDoubleEqualsToSingleEquals.fix getRangeText ChangeEqualsInFieldTypeToColon.fix - WrapExpressionInParentheses.fix getRangeText - ChangeRefCellDerefToNot.fix tryGetParseResultsForFile + WrapExpressionInParentheses.fix + ChangeRefCellDerefToNot.fix tryGetParseAndCheckResultsForFile ChangeDowncastToUpcast.fix getRangeText - MakeDeclarationMutable.fix tryGetParseResultsForFile forceGetFSharpProjectOptions - UseMutationWhenValueIsMutable.fix tryGetParseResultsForFile - ConvertInvalidRecordToAnonRecord.fix tryGetParseResultsForFile - RemoveUnnecessaryReturnOrYield.fix tryGetParseResultsForFile getLineText - ConvertCSharpLambdaToFSharpLambda.fix tryGetParseResultsForFile getLineText + MakeDeclarationMutable.fix tryGetParseAndCheckResultsForFile forceGetFSharpProjectOptions + UseMutationWhenValueIsMutable.fix tryGetParseAndCheckResultsForFile + ConvertInvalidRecordToAnonRecord.fix tryGetParseAndCheckResultsForFile + RemoveUnnecessaryReturnOrYield.fix tryGetParseAndCheckResultsForFile getLineText + ConvertCSharpLambdaToFSharpLambda.fix tryGetParseAndCheckResultsForFile getLineText AddMissingFunKeyword.fix forceFindSourceText getLineText - MakeOuterBindingRecursive.fix tryGetParseResultsForFile getLineText + MakeOuterBindingRecursive.fix tryGetParseAndCheckResultsForFile getLineText AddMissingRecKeyword.fix forceFindSourceText getLineText ConvertBangEqualsToInequality.fix getRangeText - ChangeDerefBangToValue.fix tryGetParseResultsForFile getLineText - RemoveUnusedBinding.fix tryGetParseResultsForFile - AddTypeToIndeterminateValue.fix tryGetParseResultsForFile forceGetFSharpProjectOptions - ChangeTypeOfNameToNameOf.fix tryGetParseResultsForFile + ChangeDerefBangToValue.fix tryGetParseAndCheckResultsForFile + RemoveUnusedBinding.fix tryGetParseAndCheckResultsForFile + AddTypeToIndeterminateValue.fix tryGetParseAndCheckResultsForFile forceGetFSharpProjectOptions + ChangeTypeOfNameToNameOf.fix tryGetParseAndCheckResultsForFile AddMissingInstanceMember.fix - AddMissingXmlDocumentation.fix tryGetParseResultsForFile - AddExplicitTypeAnnotation.fix tryGetParseResultsForFile - ConvertPositionalDUToNamed.fix tryGetParseResultsForFile getRangeText - ConvertTripleSlashCommentToXmlTaggedDoc.fix tryGetParseResultsForFile getRangeText - GenerateXmlDocumentation.fix tryGetParseResultsForFile - RemoveRedundantAttributeSuffix.fix tryGetParseResultsForFile + AddMissingXmlDocumentation.fix tryGetParseAndCheckResultsForFile + AddExplicitTypeAnnotation.fix tryGetParseAndCheckResultsForFile + ConvertPositionalDUToNamed.fix tryGetParseAndCheckResultsForFile + ConvertTripleSlashCommentToXmlTaggedDoc.fix tryGetParseAndCheckResultsForFile + GenerateXmlDocumentation.fix tryGetParseAndCheckResultsForFile + RemoveRedundantAttributeSuffix.fix tryGetParseAndCheckResultsForFile Run.ifEnabled (fun _ -> config.AddPrivateAccessModifier) - (AddPrivateAccessModifier.fix tryGetParseResultsForFile symbolUseWorkspace) - UseTripleQuotedInterpolation.fix tryGetParseResultsForFile getRangeText - RenameParamToMatchSignature.fix tryGetParseResultsForFile - RemovePatternArgument.fix tryGetParseResultsForFile - ToInterpolatedString.fix tryGetParseResultsForFile getLanguageVersion - AdjustConstant.fix tryGetParseResultsForFile |]) + (AddPrivateAccessModifier.fix tryGetParseAndCheckResultsForFile symbolUseWorkspace) + UseTripleQuotedInterpolation.fix tryGetParseAndCheckResultsForFile + RenameParamToMatchSignature.fix tryGetParseAndCheckResultsForFile + RemovePatternArgument.fix tryGetParseAndCheckResultsForFile + ToInterpolatedString.fix tryGetParseAndCheckResultsForFile getLanguageVersion + AdjustConstant.fix tryGetParseAndCheckResultsForFile + UpdateValueInSignatureFile.fix tryGetParseAndCheckResultsForFile |]) let forgetDocument (uri: DocumentUri) = async { diff --git a/src/FsAutoComplete/LspServers/Common.fs b/src/FsAutoComplete/LspServers/Common.fs index 3760274db..318eeda76 100644 --- a/src/FsAutoComplete/LspServers/Common.fs +++ b/src/FsAutoComplete/LspServers/Common.fs @@ -14,9 +14,27 @@ open FSharp.UMX open CliWrap +module ErrorMsgUtils = + let formatLineLookErr + (x: + {| FileName: string + Position: FcsPos |}) + = + let position = fcsPosToLsp x.Position + $"No line in {x.FileName} at position {position}" + + module Result = let ofStringErr r = r |> Result.mapError JsonRpc.Error.InternalErrorMessage + let lineLookupErr + (r: + Result<'T, {| FileName: string + Position: FcsPos |}>) + = + r + |> Result.mapError (ErrorMsgUtils.formatLineLookErr >> JsonRpc.Error.InternalErrorMessage) + let ofCoreResponse (r: CoreResponse<'a>) = match r with | CoreResponse.Res a -> Ok(Some a) @@ -84,7 +102,7 @@ type DiagnosticCollection(sendDiagnostics: DocumentUri -> Diagnostic[] -> Async< >> Log.addContext "message" exn.Message )) - mailbox.Error.Add(fun exn -> restartAgent uri) + mailbox.Error.Add(fun _exn -> restartAgent uri) mailbox and getOrAddAgent fileUri = @@ -186,7 +204,9 @@ module Helpers = let tryGetLineStr pos (text: IFSACSourceText) = text.GetLine(pos) - |> Result.ofOption (fun () -> $"No line in {text.FileName} at position {pos}") + |> Result.ofOption (fun () -> + {| FileName = text.FileName + Position = pos |}) let fullPathNormalized = Path.GetFullPath >> Utils.normalizePath >> UMX.untag diff --git a/src/FsAutoComplete/LspServers/FSharpLspClient.fs b/src/FsAutoComplete/LspServers/FSharpLspClient.fs index 0489a6a61..cbd1e2fcb 100644 --- a/src/FsAutoComplete/LspServers/FSharpLspClient.fs +++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fs @@ -132,7 +132,7 @@ type ServerProgressReport(lspClient: FSharpLspClient, ?token: ProgressToken) = match result with | Ok() -> canReportProgress <- true - | Error e -> canReportProgress <- false + | Error _e -> canReportProgress <- false if canReportProgress then do! @@ -170,7 +170,7 @@ type ServerProgressReport(lspClient: FSharpLspClient, ?token: ProgressToken) = } interface IAsyncDisposable with - member x.DisposeAsync() = task { do! x.End () (CancellationToken.None) } |> ValueTask + member x.DisposeAsync() = task { do! x.End () CancellationToken.None } |> ValueTask interface IDisposable with member x.Dispose() = (x :> IAsyncDisposable).DisposeAsync() |> ignore @@ -285,7 +285,7 @@ type ProgressListener(lspClient: FSharpLspClient, traceNamespace: string array) if activity.DisplayName |> isOneOf interestingActivities then match inflightEvents.TryRemove(activity.Id) with - | true, (old, progressReport) -> do! progressReport.End() + | true, (_old, progressReport) -> do! progressReport.End() | _ -> () | _ -> () diff --git a/src/FsAutoComplete/LspServers/FSharpLspClient.fsi b/src/FsAutoComplete/LspServers/FSharpLspClient.fsi new file mode 100644 index 000000000..984844d7c --- /dev/null +++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fsi @@ -0,0 +1,50 @@ +namespace FsAutoComplete.Lsp + +open Ionide.LanguageServerProtocol +open Ionide.LanguageServerProtocol.Server +open Ionide.LanguageServerProtocol.Types +open FsAutoComplete.LspHelpers +open System +open IcedTasks + +type FSharpLspClient = + new: sendServerNotification: ClientNotificationSender * sendServerRequest: ClientRequestSender -> FSharpLspClient + inherit LspClient + member ClientCapabilities: ClientCapabilities option with get, set + override WindowShowMessage: ShowMessageParams -> Async + override WindowShowMessageRequest: ShowMessageRequestParams -> AsyncLspResult + override WindowLogMessage: LogMessageParams -> Async + override TelemetryEvent: Newtonsoft.Json.Linq.JToken -> Async + override ClientRegisterCapability: RegistrationParams -> AsyncLspResult + override ClientUnregisterCapability: UnregistrationParams -> AsyncLspResult + override WorkspaceWorkspaceFolders: unit -> AsyncLspResult + override WorkspaceConfiguration: ConfigurationParams -> AsyncLspResult + override WorkspaceApplyEdit: ApplyWorkspaceEditParams -> AsyncLspResult + override WorkspaceSemanticTokensRefresh: unit -> Async + override TextDocumentPublishDiagnostics: PublishDiagnosticsParams -> Async + ///Custom notification for workspace/solution/project loading events + member NotifyWorkspace: p: PlainNotification -> Async + ///Custom notification for initial workspace peek + member NotifyWorkspacePeek: p: PlainNotification -> Async + member NotifyCancelledRequest: p: PlainNotification -> Async + member NotifyFileParsed: p: PlainNotification -> Async + member NotifyDocumentAnalyzed: p: DocumentAnalyzedNotification -> Async + member NotifyTestDetected: p: TestDetectedNotification -> Async + member CodeLensRefresh: unit -> Async + override WorkDoneProgressCreate: ProgressToken -> AsyncLspResult + override Progress: ProgressToken * 'Progress -> Async + +type ServerProgressReport = + new: lspClient: FSharpLspClient * ?token: ProgressToken -> ServerProgressReport + member Token: ProgressToken + member Begin: title: string * ?cancellable: bool * ?message: string * ?percentage: uint -> CancellableTask + member Report: ?cancellable: bool * ?message: string * ?percentage: uint -> CancellableTask + member End: ?message: string -> CancellableTask + interface IAsyncDisposable + interface IDisposable + +/// listener for the the events generated from the fsc ActivitySource +type ProgressListener = + new: lspClient: FSharpLspClient * traceNamespace: string array -> ProgressListener + interface IDisposable + interface IAsyncDisposable diff --git a/src/FsAutoComplete/Parser.fs b/src/FsAutoComplete/Parser.fs index 9dbfc6bbd..0ad58284b 100644 --- a/src/FsAutoComplete/Parser.fs +++ b/src/FsAutoComplete/Parser.fs @@ -30,24 +30,18 @@ module Parser = Start: FSharp.Compiler.Text.pos End: FSharp.Compiler.Text.pos } - let private setArity arity (o: #Option) = + let 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 + let inline 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 + let inline 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 inline many x = setArity ArgumentArity.OneOrMore x let verboseOption = Option([| "--verbose"; "-v"; "--debug" |], "Enable verbose logging. This is equivalent to --log-level debug.") @@ -138,8 +132,8 @@ module Parser = let dotnetPath = if - Environment.ProcessPath.EndsWith("dotnet") - || Environment.ProcessPath.EndsWith("dotnet.exe") + Environment.ProcessPath.EndsWith("dotnet", StringComparison.Ordinal) + || Environment.ProcessPath.EndsWith("dotnet.exe", StringComparison.Ordinal) then // this is valid when not running as a global tool Some(FileInfo(Environment.ProcessPath)) @@ -234,7 +228,7 @@ module Parser = 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 _excludeByLevelWhenCategory category level event = isCategory category event || not (hasMinLevel level event) let args = ctx.ParseResult diff --git a/src/FsAutoComplete/Parser.fsi b/src/FsAutoComplete/Parser.fsi new file mode 100644 index 000000000..8a6d642e3 --- /dev/null +++ b/src/FsAutoComplete/Parser.fsi @@ -0,0 +1,6 @@ +namespace FsAutoComplete + +open System.CommandLine.Parsing + +module Parser = + val parser: Parser diff --git a/src/FsAutoComplete/paket.references b/src/FsAutoComplete/paket.references index 29b7a6a7a..831027c3d 100644 --- a/src/FsAutoComplete/paket.references +++ b/src/FsAutoComplete/paket.references @@ -1,12 +1,8 @@ -#FSharpLint.Core CliWrap Destructurama.FSharp Fantomas.Client FSharp.Analyzers.SDK -FSharp.Compiler.Service -FSharp.Core FSharp.UMX -FsToolkit.ErrorHandling FsToolkit.ErrorHandling.TaskResult IcedTasks ICSharpCode.Decompiler @@ -15,16 +11,15 @@ Ionide.LanguageServerProtocol Ionide.ProjInfo.ProjectSystem Microsoft.NETFramework.ReferenceAssemblies Microsoft.SourceLink.GitHub -Newtonsoft.Json -Serilog Serilog.Sinks.Async Serilog.Sinks.Console Serilog.Sinks.File System.CommandLine -System.Configuration.ConfigurationManager FSharp.Data.Adaptive Microsoft.Extensions.Caching.Memory OpenTelemetry.Exporter.OpenTelemetryProtocol Microsoft.CodeAnalysis LinkDotNet.StringBuilder CommunityToolkit.HighPerformance +Ionide.Analyzers +FSharp.Analyzers.Build diff --git a/test/FsAutoComplete.DependencyManager.Dummy/Library.fs b/test/FsAutoComplete.DependencyManager.Dummy/Library.fs index fddd110fd..af9d4152b 100644 --- a/test/FsAutoComplete.DependencyManager.Dummy/Library.fs +++ b/test/FsAutoComplete.DependencyManager.Dummy/Library.fs @@ -4,52 +4,68 @@ open System /// A marker attribute to tell FCS that this assembly contains a Dependency Manager, or /// that a class with the attribute is a DependencyManager -[] +[] type DependencyManagerAttribute() = - inherit Attribute() + inherit Attribute() [] do () /// returned structure from the ResolveDependencies method call. -type ResolveDependenciesResult (success: bool, stdOut: string array, stdError: string array, resolutions: string seq, sourceFiles: string seq, roots: string seq) = +type ResolveDependenciesResult + ( + success: bool, + stdOut: string array, + stdError: string array, + resolutions: string seq, + sourceFiles: string seq, + roots: string seq + ) = - /// Succeeded? - member __.Success = success + /// Succeeded? + member __.Success = success - /// The resolution output log - member __.StdOut = stdOut + /// The resolution output log + member __.StdOut = stdOut - /// The resolution error log (* process stderror *) - member __.StdError = stdError + /// The resolution error log (* process stderror *) + member __.StdError = stdError - /// The resolution paths (will be treated as #r options) - member __.Resolutions = resolutions + /// The resolution paths (will be treated as #r options) + member __.Resolutions = resolutions - /// The source code file paths (will be treated as #load options) - member __.SourceFiles = sourceFiles + /// The source code file paths (will be treated as #load options) + member __.SourceFiles = sourceFiles - /// The roots to package directories (will be treated like #I options) - member __.Roots = roots + /// The roots to package directories (will be treated like #I options) + member __.Roots = roots type ScriptExtension = string type HashRLines = string seq type TFM = string -[] /// the type _must_ take an optional output directory -type DummyDependencyManager(outputDir: string option) = +[] +type DummyDependencyManager(_outputDir: string option) = - /// Name of the dependency manager - member val Name = "Dummy Dependency Manager" with get + /// Name of the dependency manager + member val Name = "Dummy Dependency Manager" with get - /// Key that identifies the types of dependencies that this DependencyManager operates on - member val Key = "dummy" with get + /// Key that identifies the types of dependencies that this DependencyManager operates on + member val Key = "dummy" with get - /// Resolve the dependencies, for the given set of arguments, go find the .dll references, scripts and additional include values. - member _.ResolveDependencies(_scriptExt: ScriptExtension, _includeLines: HashRLines, _tfm: TFM): obj = - // generally, here we'd parse the includeLines to determine what to do, - // package those results into a `ResolveDependenciesResult`, - // and return it boxed as obj. - // but here we will return a dummy - ResolveDependenciesResult(true, [|"Skipped processing of any hash-r references"|], [||], Seq.empty, Seq.empty, Seq.empty) :> _ + /// Resolve the dependencies, for the given set of arguments, go find the .dll references, scripts and additional include values. + member _.ResolveDependencies(_scriptExt: ScriptExtension, _includeLines: HashRLines, _tfm: TFM) : obj = + // generally, here we'd parse the includeLines to determine what to do, + // package those results into a `ResolveDependenciesResult`, + // and return it boxed as obj. + // but here we will return a dummy + ResolveDependenciesResult( + true, + [| "Skipped processing of any hash-r references" |], + [||], + Seq.empty, + Seq.empty, + Seq.empty + ) + :> _ diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs index a8100bb4d..e0f8c6339 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AdjustConstantTests.fs @@ -27,32 +27,36 @@ module private ConvertIntToOtherBase = | Base.Octal -> Title.Int.Convert.toOctal | Base.Binary -> Title.Int.Convert.toBinary |> CodeFix.withTitle + /// empty `expected`: no corresponding fix - let private checkBase - doc - (source: String, cursor: Range) - base' - expected - = - let name = + let private checkBase doc (source: String, cursor: Range) base' expected = + let name = if String.IsNullOrWhiteSpace expected then $"cannot convert to {base'}" else $"can convert to {base'}" - testCaseAsync name (async { - let! (doc, diags) = doc - let expected = - if String.IsNullOrWhiteSpace expected then - ExpectedResult.NotApplicable - else - ExpectedResult.After expected - do! checkFixAt - (doc, diags) - (source, cursor) + + testCaseAsync + name + (async { + let! (doc, diags) = doc + + let expected = + if String.IsNullOrWhiteSpace expected then + ExpectedResult.NotApplicable + else + ExpectedResult.After expected + + do! + checkFixAt + (doc, diags) + doc.VersionedTextDocumentIdentifier + (source, cursor) Diagnostics.acceptAll (selectIntCodeFix base') expected - }) + }) + /// empty `expectedXXX`: there should be no corresponding Fix let check server @@ -64,16 +68,17 @@ module private ConvertIntToOtherBase = (expectedBinary: String) = let (cursor, source) = Cursor.assertExtractRange beforeWithCursor - documentTestList name server (Server.createUntitledDocument source) (fun doc -> [ - checkBase doc (source, cursor) Base.Decimal expectedDecimal - checkBase doc (source, cursor) Base.Hexadecimal expectedHexadecimal - checkBase doc (source, cursor) Base.Octal expectedOctal - checkBase doc (source, cursor) Base.Binary expectedBinary - ]) + + documentTestList name server (Server.createUntitledDocument source) (fun doc -> + [ checkBase doc (source, cursor) Base.Decimal expectedDecimal + checkBase doc (source, cursor) Base.Hexadecimal expectedHexadecimal + checkBase doc (source, cursor) Base.Octal expectedOctal + checkBase doc (source, cursor) Base.Binary expectedBinary ]) + /// Checks all combinations of base': Can convert from any base to all others but not to self - /// + /// /// `template`: without cursor, but with `{number}` marker: number gets inserted here and cursor placed at end - /// + /// /// empty `valueXXX`: there should be no corresponding Fix let private checkAll server @@ -85,50 +90,59 @@ module private ConvertIntToOtherBase = (binaryNumber: String) = let applyTemplate cursor number = - let number = - if cursor then - number + "$0" - else - number + let number = if cursor then number + "$0" else number template.Replace("{number}", number) - testList name [ - let data = [(Base.Decimal, decimalNumber); (Base.Hexadecimal, hexadecimalNumber); (Base.Octal, octalNumber); (Base.Binary, binaryNumber)] - let valueOf (base') = - data - |> List.find (fun (b,_) -> b = base') - |> snd - for (base', value) in data do - if String.IsNullOrEmpty value then - () - else - let mkExpected (b) = - if base' = b || String.IsNullOrEmpty (valueOf b) then - "" - else - applyTemplate false (valueOf b) - check server $"can convert from {base'}" - (applyTemplate true value) - (mkExpected Base.Decimal) - (mkExpected Base.Hexadecimal) - (mkExpected Base.Octal) - (mkExpected Base.Binary) - ] + testList + name + [ let data = + [ (Base.Decimal, decimalNumber) + (Base.Hexadecimal, hexadecimalNumber) + (Base.Octal, octalNumber) + (Base.Binary, binaryNumber) ] + + let valueOf (base') = data |> List.find (fun (b, _) -> b = base') |> snd + + for (base', value) in data do + if String.IsNullOrEmpty value then + () + else + let mkExpected (b) = + if base' = b || String.IsNullOrEmpty(valueOf b) then + "" + else + applyTemplate false (valueOf b) + + check + server + $"can convert from {base'}" + (applyTemplate true value) + (mkExpected Base.Decimal) + (mkExpected Base.Hexadecimal) + (mkExpected Base.Octal) + (mkExpected Base.Binary) ] type private Journey = | JustDestination of string | JustSource of string | InOut of string | Neither + module private Journey = let source = function - | JustSource value | InOut value -> Some value - | JustDestination _ | Neither -> None + | JustSource value + | InOut value -> Some value + | JustDestination _ + | Neither -> None + let destination = function - | JustDestination value | InOut value -> Some value - | JustSource _ | Neither -> None + | JustDestination value + | InOut value -> Some value + | JustSource _ + | Neither -> None + let private checkAllJourneys server name @@ -142,394 +156,414 @@ module private ConvertIntToOtherBase = let number = if cursor then number + "$0" else number template.Replace("{number}", number) - testList name [ - let data = [(Base.Decimal, decimalNumber); (Base.Hexadecimal, hexadecimalNumber); (Base.Octal, octalNumber); (Base.Binary, binaryNumber)] - - for (base', j) in data do - match j |> Journey.source with - | None -> () - | Some value -> - - let mkExpected b = - if base' = b then - "" - else - data - |> List.find (fst >> (=) b) - |> snd - |> Journey.destination - |> Option.map (applyTemplate false) - |> Option.defaultValue "" - - check server $"can convert from {base'}" - (applyTemplate true value) - (mkExpected Base.Decimal) - (mkExpected Base.Hexadecimal) - (mkExpected Base.Octal) - (mkExpected Base.Binary) - ] + testList + name + [ let data = + [ (Base.Decimal, decimalNumber) + (Base.Hexadecimal, hexadecimalNumber) + (Base.Octal, octalNumber) + (Base.Binary, binaryNumber) ] + + for (base', j) in data do + match j |> Journey.source with + | None -> () + | Some value -> + + let mkExpected b = + if base' = b then + "" + else + data + |> List.find (fst >> (=) b) + |> snd + |> Journey.destination + |> Option.map (applyTemplate false) + |> Option.defaultValue "" + + check + server + $"can convert from {base'}" + (applyTemplate true value) + (mkExpected Base.Decimal) + (mkExpected Base.Hexadecimal) + (mkExpected Base.Octal) + (mkExpected Base.Binary) ] let tests state = - serverTestList "Convert int-number to other bases" state defaultConfigDto None (fun server -> [ - checkAll server "can convert simple number" - "let n = {number}" - "123" - "0x7B" - "0o173" - "0b1111011" - checkAll server "can convert simple negative number" - "let n = {number}" - "-123" - "-0x7B" - "-0o173" - "-0b1111011" - checkAll server "can convert 0" - "let n = {number}" - "0" - "0x0" - "0o0" - "0b0" - checkAll server "can convert 1" - "let n = {number}" - "1" - "0x1" - "0o1" - "0b1" - checkAll server "can convert -1" - "let n = {number}" - "-1" - "-0x1" - "-0o1" - "-0b1" - - testList "extrema" [ - // Note regarding negative `MinValue`: - // Only decimal has `-` sign -- all other should not. - // While `-0b1000_0000y` is valid -- it has basically two minus signs: one `-` and one minus bit. - // The Quick Fix removes that `-` sign when converting from decimal to other base. - // However: it does NOT remove the `-` sign when it already exists for a non-decimal base: - // `-0b1000_0000y` becomes `-0x80y`, while `0b1000_0000y` becomes `0x80y` - testList "sbyte" [ - checkAll server "can convert MaxValue" - "let n = {number} = System.SByte.MaxValue" - "127y" - "0x7Fy" - "0o177y" - "0b1111111y" - checkAll server "can convert MinValue (no `-`)" - "let n = {number} = System.SByte.MinValue" - "-128y" - "0x80y" - "0o200y" - "0b10000000y" - checkAllJourneys server "can convert MinValue (keep `-`)" - "let n = {number} = System.SByte.MinValue" - (JustDestination "-128y") - (InOut "-0x80y") - (InOut "-0o200y") - (InOut "-0b10000000y") - ] - testList "byte" [ - checkAll server "can convert MaxValue" - "let n = {number} = System.Byte.MaxValue" - "255uy" - "0xFFuy" - "0o377uy" - "0b11111111uy" - checkAll server "can convert MinValue" - "let n = {number} = System.Byte.MinValue" - "0uy" - "0x0uy" - "0o0uy" - "0b0uy" - ] - - testList "uint64" [ - checkAll server "can convert MaxValue" - "let n = {number} = System.UInt64.MaxValue" - "18446744073709551615UL" - "0xFFFFFFFFFFFFFFFFUL" - "0o1777777777777777777777UL" - "0b1111111111111111111111111111111111111111111111111111111111111111UL" - checkAll server "can convert MinValue" - "let n = {number} = System.UInt64.MinValue" - "0UL" - "0x0UL" - "0o0UL" - "0b0UL" - ] - testList "int64" [ - // let value = Int64.MinValue in sprintf "\"%i\"\n\"0x%X\"\n\"0o%o\"\n\"0b%B\"" value value value value;; - checkAll server "can convert MaxValue" - "let n = {number} = System.Int64.MaxValue" - "9223372036854775807UL" - "0x7FFFFFFFFFFFFFFFUL" - "0o777777777777777777777UL" - "0b111111111111111111111111111111111111111111111111111111111111111UL" - checkAll server "can convert MinValue (no `-`)" - "let n = {number} = System.Int64.MinValue" - "-9223372036854775808L" - "0x8000000000000000L" - "0o1000000000000000000000L" - "0b1000000000000000000000000000000000000000000000000000000000000000L" - checkAllJourneys server "can convert MinValue (keep `-`)" - "let n = {number} = System.Int64.MinValue" - (JustDestination "-9223372036854775808L") - (InOut "-0x8000000000000000L") - (InOut "-0o1000000000000000000000L") - (InOut "-0b1000000000000000000000000000000000000000000000000000000000000000L") - ] - - testList "int (without suffix)" [ - checkAll server "can convert Int64.MaxValue" - "let n = {number} = Int32.MaxValue" - "2147483647" - "0x7FFFFFFF" - "0o17777777777" - "0b1111111111111111111111111111111" - checkAll server "can convert System.Int32.MinValue" - "let n = {number} = System.Int32.MinValue" - "-2147483648" - "0x80000000" - "0o20000000000" - "0b10000000000000000000000000000000" - checkAllJourneys server "can convert MinValue (keep `-`)" - "let n = {number} = System.Int32.MinValue" - (JustDestination "-2147483648") - (InOut "-0x80000000") - (InOut "-0o20000000000") - (InOut "-0b10000000000000000000000000000000") - ] - ] - - testList "types" [ - let suffixes = [ - ("sbyte", ["y"]) - ("byte", ["uy"]) - ("int16", ["s"]) - ("uint16", ["us"]) - ("int32", [""; "l"]) - ("uint32", ["u"; "ul"]) - ("nativeint", ["n"]) - ("unativeint", ["un"]) - ("int64", ["L"]) - ("uint64", ["UL"]) - ] - - for (name, suffixes) in suffixes do - testList $"can convert {name}" [ - for suffix in suffixes do - testList $"with suffix {suffix}" [ - checkAll server $"with value 123" - $"let n = {{number}}{suffix}" - "123" - "0x7B" - "0o173" - "0b1111011" - - if not (name.StartsWith "u") && name <> "byte" then - checkAll server $"with value -123" - $"let n = {{number}}{suffix}" - "123" - "0x7B" - "0o173" - "0b1111011" - ] - ] - - testCaseAsync "does not trigger for bigint" <| - CodeFix.checkNotApplicable server - "let n = 9999999999999999999999999999$0I" - Diagnostics.acceptAll - (selectIntCodeFix Base.Hexadecimal) - ] - - testList "sign shenanigans" [ - testList "keep unnecessary sign" [ - checkAll server "keep + in +123" - "let n = {number}" - "+123" - "+0x7B" - "+0o173" - "+0b1111011" - checkAll server "keep + in +0" - "let n = {number}" - "+0" - "+0x0" - "+0o0" - "+0b0" - checkAll server "keep - in -0" - "let n = {number}" - "-0" - "-0x0" - "-0o0" - "-0b0" - checkAllJourneys server "keep + in +(-123)" - "let n = {number}" - (JustDestination "-123") - (InOut "+0xFFFFFF85") - (InOut "+0o37777777605") - (InOut "+0b11111111111111111111111110000101") - ] - - testList "explicit sign and actual sign do not match" [ - testList "keep explicit `-` in positive constant" [ - // Hex/Oct/Bin have sign bit, but can additional have explicit `-` sign - checkAllJourneys server "keep - in -(-123)" - "let n = {number}" - (JustDestination "123") - (InOut "-0xFFFFFF85") - (InOut "-0o37777777605") - (InOut "-0b11111111111111111111111110000101") - ] - testList "keep explicit `+` in negative constant" [ - checkAllJourneys server "keep + in +(-123)" - "let n = {number}" - (JustDestination "-123") - (InOut "+0xFFFFFF85") - (InOut "+0o37777777605") - (InOut "+0b11111111111111111111111110000101") - ] - ] - ] - - testList "locations" [ - check server "can convert in math expression" - "let n = max (123 + 456$0 / 13 * 17 - 9) (456 - 123)" - "" - "let n = max (123 + 0x1C8 / 13 * 17 - 9) (456 - 123)" - "let n = max (123 + 0o710 / 13 * 17 - 9) (456 - 123)" - "let n = max (123 + 0b111001000 / 13 * 17 - 9) (456 - 123)" - check server "can convert inside member" - """ + serverTestList "Convert int-number to other bases" state defaultConfigDto None (fun server -> + [ checkAll server "can convert simple number" "let n = {number}" "123" "0x7B" "0o173" "0b1111011" + checkAll server "can convert simple negative number" "let n = {number}" "-123" "-0x7B" "-0o173" "-0b1111011" + checkAll server "can convert 0" "let n = {number}" "0" "0x0" "0o0" "0b0" + checkAll server "can convert 1" "let n = {number}" "1" "0x1" "0o1" "0b1" + checkAll server "can convert -1" "let n = {number}" "-1" "-0x1" "-0o1" "-0b1" + + testList + "extrema" + [ + // Note regarding negative `MinValue`: + // Only decimal has `-` sign -- all other should not. + // While `-0b1000_0000y` is valid -- it has basically two minus signs: one `-` and one minus bit. + // The Quick Fix removes that `-` sign when converting from decimal to other base. + // However: it does NOT remove the `-` sign when it already exists for a non-decimal base: + // `-0b1000_0000y` becomes `-0x80y`, while `0b1000_0000y` becomes `0x80y` + testList + "sbyte" + [ checkAll + server + "can convert MaxValue" + "let n = {number} = System.SByte.MaxValue" + "127y" + "0x7Fy" + "0o177y" + "0b1111111y" + checkAll + server + "can convert MinValue (no `-`)" + "let n = {number} = System.SByte.MinValue" + "-128y" + "0x80y" + "0o200y" + "0b10000000y" + checkAllJourneys + server + "can convert MinValue (keep `-`)" + "let n = {number} = System.SByte.MinValue" + (JustDestination "-128y") + (InOut "-0x80y") + (InOut "-0o200y") + (InOut "-0b10000000y") ] + testList + "byte" + [ checkAll + server + "can convert MaxValue" + "let n = {number} = System.Byte.MaxValue" + "255uy" + "0xFFuy" + "0o377uy" + "0b11111111uy" + checkAll + server + "can convert MinValue" + "let n = {number} = System.Byte.MinValue" + "0uy" + "0x0uy" + "0o0uy" + "0b0uy" ] + + testList + "uint64" + [ checkAll + server + "can convert MaxValue" + "let n = {number} = System.UInt64.MaxValue" + "18446744073709551615UL" + "0xFFFFFFFFFFFFFFFFUL" + "0o1777777777777777777777UL" + "0b1111111111111111111111111111111111111111111111111111111111111111UL" + checkAll + server + "can convert MinValue" + "let n = {number} = System.UInt64.MinValue" + "0UL" + "0x0UL" + "0o0UL" + "0b0UL" ] + testList + "int64" + [ + // let value = Int64.MinValue in sprintf "\"%i\"\n\"0x%X\"\n\"0o%o\"\n\"0b%B\"" value value value value;; + checkAll + server + "can convert MaxValue" + "let n = {number} = System.Int64.MaxValue" + "9223372036854775807UL" + "0x7FFFFFFFFFFFFFFFUL" + "0o777777777777777777777UL" + "0b111111111111111111111111111111111111111111111111111111111111111UL" + checkAll + server + "can convert MinValue (no `-`)" + "let n = {number} = System.Int64.MinValue" + "-9223372036854775808L" + "0x8000000000000000L" + "0o1000000000000000000000L" + "0b1000000000000000000000000000000000000000000000000000000000000000L" + checkAllJourneys + server + "can convert MinValue (keep `-`)" + "let n = {number} = System.Int64.MinValue" + (JustDestination "-9223372036854775808L") + (InOut "-0x8000000000000000L") + (InOut "-0o1000000000000000000000L") + (InOut "-0b1000000000000000000000000000000000000000000000000000000000000000L") ] + + testList + "int (without suffix)" + [ checkAll + server + "can convert Int64.MaxValue" + "let n = {number} = Int32.MaxValue" + "2147483647" + "0x7FFFFFFF" + "0o17777777777" + "0b1111111111111111111111111111111" + checkAll + server + "can convert System.Int32.MinValue" + "let n = {number} = System.Int32.MinValue" + "-2147483648" + "0x80000000" + "0o20000000000" + "0b10000000000000000000000000000000" + checkAllJourneys + server + "can convert MinValue (keep `-`)" + "let n = {number} = System.Int32.MinValue" + (JustDestination "-2147483648") + (InOut "-0x80000000") + (InOut "-0o20000000000") + (InOut "-0b10000000000000000000000000000000") ] ] + + testList + "types" + [ let suffixes = + [ ("sbyte", [ "y" ]) + ("byte", [ "uy" ]) + ("int16", [ "s" ]) + ("uint16", [ "us" ]) + ("int32", [ ""; "l" ]) + ("uint32", [ "u"; "ul" ]) + ("nativeint", [ "n" ]) + ("unativeint", [ "un" ]) + ("int64", [ "L" ]) + ("uint64", [ "UL" ]) ] + + for (name, suffixes) in suffixes do + testList + $"can convert {name}" + [ for suffix in suffixes do + testList + $"with suffix {suffix}" + [ checkAll + server + $"with value 123" + $"let n = {{number}}{suffix}" + "123" + "0x7B" + "0o173" + "0b1111011" + + if not (name.StartsWith("u", StringComparison.Ordinal)) && name <> "byte" then + checkAll + server + $"with value -123" + $"let n = {{number}}{suffix}" + "123" + "0x7B" + "0o173" + "0b1111011" ] ] + + testCaseAsync "does not trigger for bigint" + <| CodeFix.checkNotApplicable + server + "let n = 9999999999999999999999999999$0I" + Diagnostics.acceptAll + (selectIntCodeFix Base.Hexadecimal) ] + + testList + "sign shenanigans" + [ testList + "keep unnecessary sign" + [ checkAll server "keep + in +123" "let n = {number}" "+123" "+0x7B" "+0o173" "+0b1111011" + checkAll server "keep + in +0" "let n = {number}" "+0" "+0x0" "+0o0" "+0b0" + checkAll server "keep - in -0" "let n = {number}" "-0" "-0x0" "-0o0" "-0b0" + checkAllJourneys + server + "keep + in +(-123)" + "let n = {number}" + (JustDestination "-123") + (InOut "+0xFFFFFF85") + (InOut "+0o37777777605") + (InOut "+0b11111111111111111111111110000101") ] + + testList + "explicit sign and actual sign do not match" + [ testList + "keep explicit `-` in positive constant" + [ + // Hex/Oct/Bin have sign bit, but can additional have explicit `-` sign + checkAllJourneys + server + "keep - in -(-123)" + "let n = {number}" + (JustDestination "123") + (InOut "-0xFFFFFF85") + (InOut "-0o37777777605") + (InOut "-0b11111111111111111111111110000101") ] + testList + "keep explicit `+` in negative constant" + [ checkAllJourneys + server + "keep + in +(-123)" + "let n = {number}" + (JustDestination "-123") + (InOut "+0xFFFFFF85") + (InOut "+0o37777777605") + (InOut "+0b11111111111111111111111110000101") ] ] ] + + testList + "locations" + [ check + server + "can convert in math expression" + "let n = max (123 + 456$0 / 13 * 17 - 9) (456 - 123)" + "" + "let n = max (123 + 0x1C8 / 13 * 17 - 9) (456 - 123)" + "let n = max (123 + 0o710 / 13 * 17 - 9) (456 - 123)" + "let n = max (123 + 0b111001000 / 13 * 17 - 9) (456 - 123)" + check + server + "can convert inside member" + """ type T() = member _.DoStuff(arg: int) = arg + 3 * 456$0 / 3 """ - "" - """ + "" + """ type T() = member _.DoStuff(arg: int) = arg + 3 * 0x1C8 / 3 """ - """ + """ type T() = member _.DoStuff(arg: int) = arg + 3 * 0o710 / 3 """ - """ + """ type T() = member _.DoStuff(arg: int) = arg + 3 * 0b111001000 / 3 """ - testList "can convert in enum" [ - check server "just value" - """ + testList + "can convert in enum" + [ check + server + "just value" + """ type MyEnum = | Alpha = 123 | Beta = 456$0 | Gamma = 789 """ - "" - """ + "" + """ type MyEnum = | Alpha = 123 | Beta = 0x1C8 | Gamma = 789 """ - """ + """ type MyEnum = | Alpha = 123 | Beta = 0o710 | Gamma = 789 """ - """ + """ type MyEnum = | Alpha = 123 | Beta = 0b111001000 | Gamma = 789 """ - check server "in parens" - """ + check + server + "in parens" + """ type MyEnum = | Alpha = 123 | Beta = (456$0) | Gamma = 789 """ - "" - """ + "" + """ type MyEnum = | Alpha = 123 | Beta = (0x1C8) | Gamma = 789 """ - """ + """ type MyEnum = | Alpha = 123 | Beta = (0o710) | Gamma = 789 """ - """ + """ type MyEnum = | Alpha = 123 | Beta = (0b111001000) | Gamma = 789 """ - check server "in app (lhs)" - """ + check + server + "in app (lhs)" + """ type MyEnum = | Alpha = 123 | Beta = (456$0 >>> 2) | Gamma = 789 """ - "" - """ + "" + """ type MyEnum = | Alpha = 123 | Beta = (0x1C8 >>> 2) | Gamma = 789 """ - """ + """ type MyEnum = | Alpha = 123 | Beta = (0o710 >>> 2) | Gamma = 789 """ - """ + """ type MyEnum = | Alpha = 123 | Beta = (0b111001000 >>> 2) | Gamma = 789 """ - check server "in app (rhs)" - """ + check + server + "in app (rhs)" + """ type MyEnum = | Alpha = 123 | Beta = (1 <<< 456$0) | Gamma = 789 """ - "" - """ + "" + """ type MyEnum = | Alpha = 123 | Beta = (1 <<< 0x1C8) | Gamma = 789 """ - """ + """ type MyEnum = | Alpha = 123 | Beta = (1 <<< 0o710) | Gamma = 789 """ - """ + """ type MyEnum = | Alpha = 123 | Beta = (1 <<< 0b111001000) | Gamma = 789 - """ - ] - check server "can convert in pattern" - """ + """ ] + check + server + "can convert in pattern" + """ let f arg = match arg with | 123 -> 1 @@ -537,8 +571,8 @@ module private ConvertIntToOtherBase = | 789 -> 3 | _ -> -1 """ - "" - """ + "" + """ let f arg = match arg with | 123 -> 1 @@ -546,7 +580,7 @@ module private ConvertIntToOtherBase = | 789 -> 3 | _ -> -1 """ - """ + """ let f arg = match arg with | 123 -> 1 @@ -554,7 +588,7 @@ module private ConvertIntToOtherBase = | 789 -> 3 | _ -> -1 """ - """ + """ let f arg = match arg with | 123 -> 1 @@ -562,306 +596,367 @@ module private ConvertIntToOtherBase = | 789 -> 3 | _ -> -1 """ - check server "can convert with measure" - """ - [] type km + check + server + "can convert with measure" + """ + [] type km let n = 456$0 """ - "" - """ - [] type km + "" + """ + [] type km let n = 0x1C8 """ - """ - [] type km + """ + [] type km let n = 0o710 """ - """ - [] type km + """ + [] type km let n = 0b111001000 - """ - ] - - checkAllJourneys server "does not trigger for invalid int" - // Value for invalid `SynConst` is always `0` -> cannot convert - "let n = {number}" - (JustSource "1099511627775") - (JustSource "0xFFFFFFFFFF") - (JustSource "0o17777777777777") - (JustSource "0b1111111111111111111111111111111111111111") - - testCaseAsync "does not trigger on comment after constant" <| - CodeFix.checkNotApplicable server + """ ] + + checkAllJourneys + server + "does not trigger for invalid int" + // Value for invalid `SynConst` is always `0` -> cannot convert + "let n = {number}" + (JustSource "1099511627775") + (JustSource "0xFFFFFFFFFF") + (JustSource "0o17777777777777") + (JustSource "0b1111111111111111111111111111111111111111") + + testCaseAsync "does not trigger on comment after constant" + <| CodeFix.checkNotApplicable + server "let n = 123 // some$0 comment" Diagnostics.acceptAll (selectIntCodeFix Base.Hexadecimal) - testList "different upper-lower-cases in bases" [ - testList "hexadecimal" [ - testCaseAsync "0x" <| - CodeFix.checkApplicable server - "let n = 0x123$0" - Diagnostics.acceptAll - (selectIntCodeFix Base.Decimal) - testCaseAsync "0X" <| - CodeFix.checkApplicable server - "let n = 0X123$0" - Diagnostics.acceptAll - (selectIntCodeFix Base.Decimal) - ] - testList "octal" [ - testCaseAsync "0o" <| - CodeFix.checkApplicable server - "let n = 0o443$0" - Diagnostics.acceptAll - (selectIntCodeFix Base.Decimal) - testCaseAsync "0O" <| - CodeFix.checkApplicable server - "let n = 0O443$0" - Diagnostics.acceptAll - (selectIntCodeFix Base.Decimal) - ] - testList "binary" [ - testCaseAsync "0b" <| - CodeFix.checkApplicable server - "let n = 0b100100011$0" - Diagnostics.acceptAll - (selectIntCodeFix Base.Decimal) - testCaseAsync "0B" <| - CodeFix.checkApplicable server - "let n = 0B100100011$0" - Diagnostics.acceptAll - (selectIntCodeFix Base.Decimal) - ] - ] - ]) + testList + "different upper-lower-cases in bases" + [ testList + "hexadecimal" + [ testCaseAsync "0x" + <| CodeFix.checkApplicable + server + "let n = 0x123$0" + Diagnostics.acceptAll + (selectIntCodeFix Base.Decimal) + testCaseAsync "0X" + <| CodeFix.checkApplicable + server + "let n = 0X123$0" + Diagnostics.acceptAll + (selectIntCodeFix Base.Decimal) ] + testList + "octal" + [ testCaseAsync "0o" + <| CodeFix.checkApplicable + server + "let n = 0o443$0" + Diagnostics.acceptAll + (selectIntCodeFix Base.Decimal) + testCaseAsync "0O" + <| CodeFix.checkApplicable + server + "let n = 0O443$0" + Diagnostics.acceptAll + (selectIntCodeFix Base.Decimal) ] + testList + "binary" + [ testCaseAsync "0b" + <| CodeFix.checkApplicable + server + "let n = 0b100100011$0" + Diagnostics.acceptAll + (selectIntCodeFix Base.Decimal) + testCaseAsync "0B" + <| CodeFix.checkApplicable + server + "let n = 0B100100011$0" + Diagnostics.acceptAll + (selectIntCodeFix Base.Decimal) ] ] ]) module Float = let tests state = - serverTestList "Convert float-number in Hex/Oct/Bin to other bases" state defaultConfigDto None (fun server -> [ - // Note: No Decimal: cannot be represented as Hex/Oct/Bin + serverTestList "Convert float-number in Hex/Oct/Bin to other bases" state defaultConfigDto None (fun server -> + [ + // Note: No Decimal: cannot be represented as Hex/Oct/Bin + + let checkAll server name template (hexadecimalNumber: String) (octalNumber: String) (binaryNumber: String) = + checkAllJourneys + server + name + template + (Neither) + (InOut hexadecimalNumber) + (InOut octalNumber) + (InOut binaryNumber) + + testList + "can convert pi" + [ + // let value = Math.PI in let bits = BitConverter.DoubleToUInt64Bits(value) in [ $"0x%X{bits}LF"; $"0o%o{bits}LF"; $"0b%B{bits}LF" ];; + checkAll + server + "float" + "let n = {number}" + "0x400921FB54442D18LF" + "0o400111037552421026430LF" + "0b100000000001001001000011111101101010100010001000010110100011000LF" + // let value = MathF.PI in let bits = BitConverter.SingleToUInt32Bits(value) in [ $"0x%X{bits}lf"; $"0o%o{bits}lf"; $"0b%B{bits}lf" ];; + checkAll + server + "float32" + "let n = {number}" + "0x40490FDBlf" + "0o10022207733lf" + "0b1000000010010010000111111011011lf" ] + + testList + "can convert 0" + [ checkAll server "float" "let n = {number}" "0x0LF" "0o0LF" "0b0LF" + checkAll server "float32" "let n = {number}" "0x0lf" "0o0lf" "0b0lf" ] + + testList + "can convert -pi" + [ checkAll + server + "float" + "let n = {number}" + "0xC00921FB54442D18LF" + "0o1400111037552421026430LF" + "0b1100000000001001001000011111101101010100010001000010110100011000LF" + checkAll + server + "float32" + "let n = {number}" + "0xC0490FDBlf" + "0o30022207733lf" + "0b11000000010010010000111111011011lf" + + testList + "keep existing `-`" + [ checkAll + server + "float" + "let n = {number}" + "-0x400921FB54442D18LF" + "-0o400111037552421026430LF" + "-0b100000000001001001000011111101101010100010001000010110100011000LF" + checkAll + server + "float32" + "let n = {number}" + "-0x40490FDBlf" + "-0o10022207733lf" + "-0b1000000010010010000111111011011lf" ] ] + + testList + "can convert MaxValue" + [ checkAll + server + "float" + "let n = {number}" + "0x7FEFFFFFFFFFFFFFLF" + "0o777577777777777777777LF" + "0b111111111101111111111111111111111111111111111111111111111111111LF" + checkAll + server + "float32" + "let n = {number}" + "0x7F7FFFFFlf" + "0o17737777777lf" + "0b1111111011111111111111111111111lf" ] + + testList + "can convert MinValue" + [ checkAll + server + "float" + "let n = {number}" + "0xFFEFFFFFFFFFFFFFLF" + "0o1777577777777777777777LF" + "0b1111111111101111111111111111111111111111111111111111111111111111LF" + checkAll + server + "float32" + "let n = {number}" + "0xFF7FFFFFlf" + "0o37737777777lf" + "0b11111111011111111111111111111111lf" + + testList + "keep existing `-`" + [ + // Note: unlike int numbers: float is symmetric: `MinValue = - MaxValue` -> just negative bit changed + checkAll + server + "float" + "let n = {number}" + "-0x7FEFFFFFFFFFFFFFLF" + "-0o777577777777777777777LF" + "-0b111111111101111111111111111111111111111111111111111111111111111LF" + checkAll + server + "float32" + "let n = {number}" + "-0x7F7FFFFFlf" + "-0o17737777777lf" + "-0b1111111011111111111111111111111lf" ] ] + + testList + "can convert nan" + [ + // `nan`, `nanf` + checkAll + server + "float - nan" + "let n = {number}" + "0xFFF8000000000000LF" + "0o1777700000000000000000LF" + "0b1111111111111000000000000000000000000000000000000000000000000000LF" + checkAll + server + "float32 - nanf" + "let n = {number}" + "0xFFC00000lf" + "0o37760000000lf" + "0b11111111110000000000000000000000lf" + + // `nan` that are different from default F# `nan` (-> tests above) + checkAll + server + "float - different nan" + "let n = {number}" + "0xFFF800C257000000LF" + "0o1777700014112700000000LF" + "0b1111111111111000000000001100001001010111000000000000000000000000LF" + checkAll + server + "float32 -- different nan" + "let n = {number}" + "0xFFC00000lf" + "0o37760000000lf" + "0b11111111110000000000000000000000lf" - let checkAll - server - name template - (hexadecimalNumber: String) - (octalNumber: String) - (binaryNumber: String) - = - checkAllJourneys server name template - (Neither) - (InOut hexadecimalNumber) - (InOut octalNumber) - (InOut binaryNumber) - - testList "can convert pi" [ - // let value = Math.PI in let bits = BitConverter.DoubleToUInt64Bits(value) in [ $"0x%X{bits}LF"; $"0o%o{bits}LF"; $"0b%B{bits}LF" ];; - checkAll server "float" - "let n = {number}" - "0x400921FB54442D18LF" - "0o400111037552421026430LF" - "0b100000000001001001000011111101101010100010001000010110100011000LF" - // let value = MathF.PI in let bits = BitConverter.SingleToUInt32Bits(value) in [ $"0x%X{bits}lf"; $"0o%o{bits}lf"; $"0b%B{bits}lf" ];; - checkAll server "float32" - "let n = {number}" - "0x40490FDBlf" - "0o10022207733lf" - "0b1000000010010010000111111011011lf" - ] - testList "can convert 0" [ - checkAll server "float" - "let n = {number}" - "0x0LF" - "0o0LF" - "0b0LF" - checkAll server "float32" - "let n = {number}" - "0x0lf" - "0o0lf" - "0b0lf" - ] - testList "can convert -pi" [ - checkAll server "float" - "let n = {number}" - "0xC00921FB54442D18LF" - "0o1400111037552421026430LF" - "0b1100000000001001001000011111101101010100010001000010110100011000LF" - checkAll server "float32" - "let n = {number}" - "0xC0490FDBlf" - "0o30022207733lf" - "0b11000000010010010000111111011011lf" - - testList "keep existing `-`" [ - checkAll server "float" - "let n = {number}" - "-0x400921FB54442D18LF" - "-0o400111037552421026430LF" - "-0b100000000001001001000011111101101010100010001000010110100011000LF" - checkAll server "float32" - "let n = {number}" - "-0x40490FDBlf" - "-0o10022207733lf" - "-0b1000000010010010000111111011011lf" - ] - ] - - testList "can convert MaxValue" [ - checkAll server "float" - "let n = {number}" - "0x7FEFFFFFFFFFFFFFLF" - "0o777577777777777777777LF" - "0b111111111101111111111111111111111111111111111111111111111111111LF" - checkAll server "float32" - "let n = {number}" - "0x7F7FFFFFlf" - "0o17737777777lf" - "0b1111111011111111111111111111111lf" - ] - testList "can convert MinValue" [ - checkAll server "float" - "let n = {number}" - "0xFFEFFFFFFFFFFFFFLF" - "0o1777577777777777777777LF" - "0b1111111111101111111111111111111111111111111111111111111111111111LF" - checkAll server "float32" - "let n = {number}" - "0xFF7FFFFFlf" - "0o37737777777lf" - "0b11111111011111111111111111111111lf" - - testList "keep existing `-`" [ - // Note: unlike int numbers: float is symmetric: `MinValue = - MaxValue` -> just negative bit changed - checkAll server "float" - "let n = {number}" - "-0x7FEFFFFFFFFFFFFFLF" - "-0o777577777777777777777LF" - "-0b111111111101111111111111111111111111111111111111111111111111111LF" - checkAll server "float32" - "let n = {number}" - "-0x7F7FFFFFlf" - "-0o17737777777lf" - "-0b1111111011111111111111111111111lf" - ] - ] - - testList "can convert nan" [ - // `nan`, `nanf` - checkAll server "float - nan" - "let n = {number}" - "0xFFF8000000000000LF" - "0o1777700000000000000000LF" - "0b1111111111111000000000000000000000000000000000000000000000000000LF" - checkAll server "float32 - nanf" - "let n = {number}" - "0xFFC00000lf" - "0o37760000000lf" - "0b11111111110000000000000000000000lf" - - // `nan` that are different from default F# `nan` (-> tests above) - checkAll server "float - different nan" - "let n = {number}" - "0xFFF800C257000000LF" - "0o1777700014112700000000LF" - "0b1111111111111000000000001100001001010111000000000000000000000000LF" - checkAll server "float32 -- different nan" - "let n = {number}" - "0xFFC00000lf" - "0o37760000000lf" - "0b11111111110000000000000000000000lf" - - ] - testList "can convert infinity" [ - testList "+" [ - checkAll server "float" - "let n = {number}" - "0x7FF0000000000000LF" - "0o777600000000000000000LF" - "0b111111111110000000000000000000000000000000000000000000000000000LF" - checkAll server "float32" - "let n = {number}" - "0x7F800000lf" - "0o17740000000lf" - "0b1111111100000000000000000000000lf" - ] - testList "-" [ - checkAll server "float" - "let n = {number}" - "0xFFF0000000000000LF" - "0o1777600000000000000000LF" - "0b1111111111110000000000000000000000000000000000000000000000000000LF" - checkAll server "float32" - "let n = {number}" - "0xFF800000lf" - "0o37740000000lf" - "0b11111111100000000000000000000000lf" - ] - ] - ]) + ] + + testList + "can convert infinity" + [ testList + "+" + [ checkAll + server + "float" + "let n = {number}" + "0x7FF0000000000000LF" + "0o777600000000000000000LF" + "0b111111111110000000000000000000000000000000000000000000000000000LF" + checkAll + server + "float32" + "let n = {number}" + "0x7F800000lf" + "0o17740000000lf" + "0b1111111100000000000000000000000lf" ] + testList + "-" + [ checkAll + server + "float" + "let n = {number}" + "0xFFF0000000000000LF" + "0o1777600000000000000000LF" + "0b1111111111110000000000000000000000000000000000000000000000000000LF" + checkAll + server + "float32" + "let n = {number}" + "0xFF800000lf" + "0o37740000000lf" + "0b11111111100000000000000000000000lf" ] ] ]) module private ConvertCharToOtherForm = let private tryExtractChar (title: String) = let (start, fin) = "Convert to `", "`" - if title.StartsWith start && title.EndsWith fin then - let c = title.Substring(start.Length, title.Length - start.Length - fin.Length).ToString() + + if + title.StartsWith(start, StringComparison.Ordinal) + && title.EndsWith(fin, StringComparison.Ordinal) + then let c = - if c.Length > 3 && c.StartsWith "'" && c.EndsWith "'B" then + title + .Substring(start.Length, title.Length - start.Length - fin.Length) + .ToString() + + let c = + if + c.Length > 3 + && c.StartsWith("'", StringComparison.Ordinal) + && c.EndsWith("'B", StringComparison.Ordinal) + then // byte char (only when converting from int to char representation. Otherwise no `B` suffix in title) c.Substring(1, c.Length - 2) else c - c - |> Some + + c |> Some else None + let private extractFormat (char: String) = - if char.StartsWith "\\u" then + if char.StartsWith("\\u", StringComparison.Ordinal) then CharFormat.Utf16Hexadecimal - elif char.StartsWith "\\U" then + elif char.StartsWith("\\U", StringComparison.Ordinal) then CharFormat.Utf32Hexadecimal - elif char.StartsWith "\\x" then + elif char.StartsWith("\\x", StringComparison.Ordinal) then CharFormat.Hexadecimal elif char.Length >= 2 && char[0] = '\\' && Char.IsDigit char[1] then CharFormat.Decimal else CharFormat.Char - let private tryExtractCharAndFormat (title: String) = - tryExtractChar title - |> Option.map (fun c -> c, extractFormat c) - + + let private tryExtractCharAndFormat (title: String) = tryExtractChar title |> Option.map (fun c -> c, extractFormat c) + let selectCharCodeFix (format: CharFormat) = let f (a: CodeAction) = a.Title - |> tryExtractCharAndFormat + |> tryExtractCharAndFormat |> Option.map (snd >> (=) format) |> Option.defaultValue false + CodeFix.matching f - let private checkFormat - doc - (source: String, cursor: Range) - (format: CharFormat) - expected - = - let name = + let private checkFormat doc (source: String, cursor: Range) (format: CharFormat) expected = + let name = if String.IsNullOrWhiteSpace expected then $"cannot convert to {format}" else $"can convert to {format}" - testCaseAsync name (async { - let! (doc, diags) = doc - let expected = - if String.IsNullOrWhiteSpace expected then - ExpectedResult.NotApplicable - else - ExpectedResult.After expected - do! checkFixAt - (doc, diags) - (source, cursor) + + testCaseAsync + name + (async { + let! (doc, diags) = doc + + let expected = + if String.IsNullOrWhiteSpace expected then + ExpectedResult.NotApplicable + else + ExpectedResult.After expected + + do! + checkFixAt + (doc, diags) + doc.VersionedTextDocumentIdentifier + (source, cursor) Diagnostics.acceptAll (selectCharCodeFix (format)) expected - }) + }) let check server @@ -874,13 +969,14 @@ module private ConvertCharToOtherForm = (expectedUtf32Hexadecimal: String) = let (cursor, source) = Cursor.assertExtractRange beforeWithCursor - documentTestList name server (Server.createUntitledDocument source) (fun doc -> [ - checkFormat doc (source, cursor) (CharFormat.Char) expectedChar - checkFormat doc (source, cursor) (CharFormat.Decimal) expectedDecimal - checkFormat doc (source, cursor) (CharFormat.Hexadecimal) expectedHexadecimal - checkFormat doc (source, cursor) (CharFormat.Utf16Hexadecimal) expectedUtf16Hexadecimal - checkFormat doc (source, cursor) (CharFormat.Utf32Hexadecimal) expectedUtf32Hexadecimal - ]) + + documentTestList name server (Server.createUntitledDocument source) (fun doc -> + [ checkFormat doc (source, cursor) (CharFormat.Char) expectedChar + checkFormat doc (source, cursor) (CharFormat.Decimal) expectedDecimal + checkFormat doc (source, cursor) (CharFormat.Hexadecimal) expectedHexadecimal + checkFormat doc (source, cursor) (CharFormat.Utf16Hexadecimal) expectedUtf16Hexadecimal + checkFormat doc (source, cursor) (CharFormat.Utf32Hexadecimal) expectedUtf32Hexadecimal ]) + /// in `template`: use `{char}` as placeholder let private checkAll server @@ -893,699 +989,752 @@ module private ConvertCharToOtherForm = (utf32HexadecimalValue: String) = let applyTemplate cursor number = - let number = - if cursor then - number + "$0" - else - number + let number = if cursor then number + "$0" else number template.Replace("{char}", number) - testList name [ - let data = [ - CharFormat.Char, charValue - CharFormat.Decimal, decimalValue - CharFormat.Hexadecimal, hexadecimalValue - CharFormat.Utf16Hexadecimal, utf16HexadecimalValue - CharFormat.Utf32Hexadecimal, utf32HexadecimalValue - ] - let valueOf (format) = - data - |> List.find (fun (b,_) -> b = format) - |> snd - for (format, value) in data do - if String.IsNullOrEmpty value then - () - else - let mkExpected (f) = - if format = f || String.IsNullOrEmpty (valueOf f) then - "" - else - applyTemplate false (valueOf f) - check server $"can convert from {format}" - (applyTemplate true value) - (mkExpected CharFormat.Char) - (mkExpected CharFormat.Decimal) - (mkExpected CharFormat.Hexadecimal) - (mkExpected CharFormat.Utf16Hexadecimal) - (mkExpected CharFormat.Utf32Hexadecimal) - ] - + testList + name + [ let data = + [ CharFormat.Char, charValue + CharFormat.Decimal, decimalValue + CharFormat.Hexadecimal, hexadecimalValue + CharFormat.Utf16Hexadecimal, utf16HexadecimalValue + CharFormat.Utf32Hexadecimal, utf32HexadecimalValue ] + + let valueOf (format) = data |> List.find (fun (b, _) -> b = format) |> snd + + for (format, value) in data do + if String.IsNullOrEmpty value then + () + else + let mkExpected (f) = + if format = f || String.IsNullOrEmpty(valueOf f) then + "" + else + applyTemplate false (valueOf f) + + check + server + $"can convert from {format}" + (applyTemplate true value) + (mkExpected CharFormat.Char) + (mkExpected CharFormat.Decimal) + (mkExpected CharFormat.Hexadecimal) + (mkExpected CharFormat.Utf16Hexadecimal) + (mkExpected CharFormat.Utf32Hexadecimal) ] + let tests state = - serverTestList "Convert char" state defaultConfigDto None (fun server -> [ - checkAll server "can convert ç" - "let c = '{char}'" - "ç" - "\\231" - "\\xE7" - "\\u00E7" - "\\U000000E7" - checkAll server "can convert \\n" - "let c = '{char}'" - "\\n" - "\\010" - "\\x0A" - "\\u000A" - "\\U0000000A" - checkAll server "can convert \\000 except to char" - "let c = '{char}'" - "" - "\\000" - "\\x00" - "\\u0000" - "\\U00000000" - - checkAll server "can convert \\u2248 only to formats that are big enough" - "let c = '{char}'" - "≈" - "" - "" - "\\u2248" - "\\U00002248" - - checkAll server "can convert single quotation mark" - "let c = '{char}'" - "\\\'" - "\\039" - "\\x27" - "\\u0027" - "\\U00000027" - - checkAll server "can convert unescaped double quotation mark" - "let c = '{char}'" - "\"" - "\\034" - "\\x22" - "\\u0022" - "\\U00000022" - // Note: Just check from `'"` to number forms. - // Other directions produce unescaped quotation mark - // -> Handled in test above - check server "can convert escaped double quotation mark" - "let c = '\"$0'" - "" - "let c = '\\034'" - "let c = '\\x22'" - "let c = '\\u0022'" - "let c = '\\U00000022'" - - testList "byte" [ - let checkAll + serverTestList "Convert char" state defaultConfigDto None (fun server -> + [ checkAll server "can convert ç" "let c = '{char}'" "ç" "\\231" "\\xE7" "\\u00E7" "\\U000000E7" + checkAll server "can convert \\n" "let c = '{char}'" "\\n" "\\010" "\\x0A" "\\u000A" "\\U0000000A" + checkAll server - name - (template: String) - (charValue: String) - (decimalValue: String) - (hexadecimalValue: String) - (utf16HexadecimalValue: String) - (utf32HexadecimalValue: String) - = - // Note: `\x` & `\U` are currently not supported for byte char - //TODO: change once supported was added - checkAll server name template - charValue - decimalValue - "" - utf16HexadecimalValue - "" - - checkAll server "can convert f" - "let c = '{char}'B" - "f" - "\\102" - "\\x66" - "\\u0066" - "\\U00000066" - checkAll server "can convert \\n" - "let c = '{char}'B" - "\\n" - "\\010" - "\\x0A" - "\\u000A" - "\\U0000000A" - checkAll server "can convert \\000 except to char" - "let c = '{char}'B" + "can convert \\000 except to char" + "let c = '{char}'" "" "\\000" "\\x00" "\\u0000" "\\U00000000" - check server "does not trigger for char outside of byte range" - "let c = 'ç$0'B" - "" "" "" "" "" - ] - ]) + + checkAll + server + "can convert \\u2248 only to formats that are big enough" + "let c = '{char}'" + "≈" + "" + "" + "\\u2248" + "\\U00002248" + + checkAll + server + "can convert single quotation mark" + "let c = '{char}'" + "\\\'" + "\\039" + "\\x27" + "\\u0027" + "\\U00000027" + + checkAll + server + "can convert unescaped double quotation mark" + "let c = '{char}'" + "\"" + "\\034" + "\\x22" + "\\u0022" + "\\U00000022" + // Note: Just check from `'"` to number forms. + // Other directions produce unescaped quotation mark + // -> Handled in test above + check + server + "can convert escaped double quotation mark" + "let c = '\"$0'" + "" + "let c = '\\034'" + "let c = '\\x22'" + "let c = '\\u0022'" + "let c = '\\U00000022'" + + testList + "byte" + [ let checkAll + server + name + (template: String) + (charValue: String) + (decimalValue: String) + (_hexadecimalValue: String) + (utf16HexadecimalValue: String) + (_utf32HexadecimalValue: String) + = + // Note: `\x` & `\U` are currently not supported for byte char + //TODO: change once supported was added + checkAll server name template charValue decimalValue "" utf16HexadecimalValue "" + + checkAll server "can convert f" "let c = '{char}'B" "f" "\\102" "\\x66" "\\u0066" "\\U00000066" + checkAll server "can convert \\n" "let c = '{char}'B" "\\n" "\\010" "\\x0A" "\\u000A" "\\U0000000A" + + checkAll + server + "can convert \\000 except to char" + "let c = '{char}'B" + "" + "\\000" + "\\x00" + "\\u0000" + "\\U00000000" + + check server "does not trigger for char outside of byte range" "let c = 'ç$0'B" "" "" "" "" "" ] ]) module private ConvertByteBetweenIntAndChar = let tests state = - serverTestList "Convert Byte between Int And Char" state defaultConfigDto None (fun server -> [ - let template = sprintf "let c = %s" - let charTemplate (c: string) = template $"'%s{c}'B" - ConvertCharToOtherForm.check server "can convert from int to char" - (template "102$0uy") - (charTemplate "f") - (charTemplate "\\102") - ""// (charTemplate "\\x66") - (charTemplate "\\u0066") - ""// (charTemplate "\\U00000066") - - let template = sprintf "let c = %s" - let intTemplate (c: string) = template $"%s{c}uy" - ConvertIntToOtherBase.check server "can convert from char to int" - (template "'f$0'B") - (intTemplate "102") - (intTemplate "0x66") - (intTemplate "0o146") - (intTemplate "0b1100110") - - testCaseAsync "cannot convert from int > 127 to char" <| - CodeFix.checkNotApplicable server + serverTestList "Convert Byte between Int And Char" state defaultConfigDto None (fun server -> + [ let template = sprintf "let c = %s" + let charTemplate (c: string) = template $"'%s{c}'B" + + ConvertCharToOtherForm.check + server + "can convert from int to char" + (template "102$0uy") + (charTemplate "f") + (charTemplate "\\102") + "" // (charTemplate "\\x66") + (charTemplate "\\u0066") + "" // (charTemplate "\\U00000066") + + let template = sprintf "let c = %s" + let intTemplate (c: string) = template $"%s{c}uy" + + ConvertIntToOtherBase.check + server + "can convert from char to int" + (template "'f$0'B") + (intTemplate "102") + (intTemplate "0x66") + (intTemplate "0o146") + (intTemplate "0b1100110") + + testCaseAsync "cannot convert from int > 127 to char" + <| CodeFix.checkNotApplicable + server "let c = 250$0uy" Diagnostics.acceptAll (ConvertCharToOtherForm.selectCharCodeFix CharFormat.Char) - testCaseAsync "cannot convert from char > 127 to int" <| - CodeFix.checkNotApplicable server + + testCaseAsync "cannot convert from char > 127 to int" + <| CodeFix.checkNotApplicable + server "let c = 'ú$0'B;" Diagnostics.acceptAll - (ConvertIntToOtherBase.selectIntCodeFix Base.Decimal) - ]) + (ConvertIntToOtherBase.selectIntCodeFix Base.Decimal) ]) module private AddDigitGroupSeparator = let private intTests state = - serverTestList "To int numbers" state defaultConfigDto None (fun server -> [ - testCaseAsync "can add separator to long decimal int" <| - CodeFix.check server + serverTestList "To int numbers" state defaultConfigDto None (fun server -> + [ testCaseAsync "can add separator to long decimal int" + <| CodeFix.check + server "let value = 1234567890$0" Diagnostics.acceptAll (CodeFix.withTitle Title.Int.Separate.decimal3) "let value = 1_234_567_890" - testCaseAsync "cannot add separator short decimal int" <| - CodeFix.checkNotApplicable server + testCaseAsync "cannot add separator short decimal int" + <| CodeFix.checkNotApplicable + server "let value = 123$0" Diagnostics.acceptAll (CodeFix.withTitle Title.Int.Separate.decimal3) - testCaseAsync "cannot add separator to decimal int with existing separator" <| - CodeFix.checkNotApplicable server + testCaseAsync "cannot add separator to decimal int with existing separator" + <| CodeFix.checkNotApplicable + server "let value = 123456789_0$0" Diagnostics.acceptAll (CodeFix.withTitle Title.Int.Separate.decimal3) - testCaseAsync "can add separator to long negative decimal int" <| - CodeFix.check server + testCaseAsync "can add separator to long negative decimal int" + <| CodeFix.check + server "let value = -1234567890$0" Diagnostics.acceptAll (CodeFix.withTitle Title.Int.Separate.decimal3) "let value = -1_234_567_890" - testCaseAsync "can add separator to decimal int with leading zeros" <| - CodeFix.check server + testCaseAsync "can add separator to decimal int with leading zeros" + <| CodeFix.check + server "let value = 0000000090$0" Diagnostics.acceptAll (CodeFix.withTitle Title.Int.Separate.decimal3) "let value = 0_000_000_090" - testCaseAsync "can add separator to too-long decimal int" <| - CodeFix.check server + testCaseAsync "can add separator to too-long decimal int" + <| CodeFix.check + server "let value = 12345678901234567890$0" Diagnostics.acceptAll (CodeFix.withTitle Title.Int.Separate.decimal3) "let value = 12_345_678_901_234_567_890" - testCaseAsync "can add separator to long decimal int64" <| - CodeFix.check server + testCaseAsync "can add separator to long decimal int64" + <| CodeFix.check + server "let value = 12345678901234567L$0" Diagnostics.acceptAll (CodeFix.withTitle Title.Int.Separate.decimal3) "let value = 12_345_678_901_234_567L" - testList "can add separator to hexadecimal int" [ - testCaseAsync "words" <| - CodeFix.check server - "let value = 0x1234578$0" - Diagnostics.acceptAll - (CodeFix.withTitle Title.Int.Separate.hexadecimal4) - "let value = 0x123_4578" - testCaseAsync "bytes" <| - CodeFix.check server - "let value = 0x1234578$0" - Diagnostics.acceptAll - (CodeFix.withTitle Title.Int.Separate.hexadecimal2) - "let value = 0x1_23_45_78" - ] - testCaseAsync "can add separator to octal int" <| - CodeFix.check server + testList + "can add separator to hexadecimal int" + [ testCaseAsync "words" + <| CodeFix.check + server + "let value = 0x1234578$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Separate.hexadecimal4) + "let value = 0x123_4578" + testCaseAsync "bytes" + <| CodeFix.check + server + "let value = 0x1234578$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Separate.hexadecimal2) + "let value = 0x1_23_45_78" ] + testCaseAsync "can add separator to octal int" + <| CodeFix.check + server "let value = 0o1234567$0" Diagnostics.acceptAll (CodeFix.withTitle Title.Int.Separate.octal3) "let value = 0o1_234_567" - testList "can add separator to binary int" [ - testCaseAsync "nibbles" <| - CodeFix.check server - "let value = 0b1010101010101010101$0" - Diagnostics.acceptAll - (CodeFix.withTitle Title.Int.Separate.binary4) - "let value = 0b101_0101_0101_0101_0101" - testCaseAsync "bytes" <| - CodeFix.check server - "let value = 0b1010101010101010101$0" - Diagnostics.acceptAll - (CodeFix.withTitle Title.Int.Separate.binary8) - "let value = 0b101_01010101_01010101" - ] - testCaseAsync "can add separator to bigint" <| - CodeFix.check server + testList + "can add separator to binary int" + [ testCaseAsync "nibbles" + <| CodeFix.check + server + "let value = 0b1010101010101010101$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Separate.binary4) + "let value = 0b101_0101_0101_0101_0101" + testCaseAsync "bytes" + <| CodeFix.check + server + "let value = 0b1010101010101010101$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Separate.binary8) + "let value = 0b101_01010101_01010101" ] + testCaseAsync "can add separator to bigint" + <| CodeFix.check + server "let value = 9999999999999999999999999999$0I" Diagnostics.acceptAll (CodeFix.withTitle Title.Int.Separate.decimal3) "let value = 9_999_999_999_999_999_999_999_999_999I" - testCaseAsync "does not trigger for short number" <| - CodeFix.checkNotApplicable server + testCaseAsync "does not trigger for short number" + <| CodeFix.checkNotApplicable + server "let value = 123$0" Diagnostics.acceptAll - (CodeFix.withTitle Title.Int.Separate.decimal3) - ]) + (CodeFix.withTitle Title.Int.Separate.decimal3) ]) let private floatTests state = - serverTestList "To float numbers" state defaultConfigDto None (fun server -> [ - testCaseAsync "can add separator to X.X float" <| - CodeFix.check server + serverTestList "To float numbers" state defaultConfigDto None (fun server -> + [ testCaseAsync "can add separator to X.X float" + <| CodeFix.check + server "let value = 1234567.01234567$0" Diagnostics.acceptAll (CodeFix.withTitle Title.Float.Separate.all3) "let value = 1_234_567.012_345_67" - testCaseAsync "can add separator to X.XeX float" <| - CodeFix.check server + testCaseAsync "can add separator to X.XeX float" + <| CodeFix.check + server "let value = 1234567.01234567e12345678$0" Diagnostics.acceptAll (CodeFix.withTitle Title.Float.Separate.all3) "let value = 1_234_567.012_345_67e12_345_678" - testCaseAsync "can add separator to X. float" <| - CodeFix.check server + testCaseAsync "can add separator to X. float" + <| CodeFix.check + server "let value = 1234567.$0" Diagnostics.acceptAll (CodeFix.withTitle Title.Float.Separate.all3) "let value = 1_234_567." - testCaseAsync "can add separator to XeX float" <| - CodeFix.check server + testCaseAsync "can add separator to XeX float" + <| CodeFix.check + server "let value = 1234567e12345678$0" Diagnostics.acceptAll (CodeFix.withTitle Title.Float.Separate.all3) "let value = 1_234_567e12_345_678" - testCaseAsync "can add separator to float32" <| - CodeFix.check server + testCaseAsync "can add separator to float32" + <| CodeFix.check + server "let value = 1234567.01234567f$0" Diagnostics.acceptAll (CodeFix.withTitle Title.Float.Separate.all3) "let value = 1_234_567.012_345_67f" - testCaseAsync "can add separator to decimal" <| - CodeFix.check server + testCaseAsync "can add separator to decimal" + <| CodeFix.check + server "let value = 1234567.01234567m$0" Diagnostics.acceptAll (CodeFix.withTitle Title.Float.Separate.all3) "let value = 1_234_567.012_345_67m" - testCaseAsync "keep sign" <| - CodeFix.check server + testCaseAsync "keep sign" + <| CodeFix.check + server "let value = -1234567.01234567e12345678$0" Diagnostics.acceptAll (CodeFix.withTitle Title.Float.Separate.all3) "let value = -1_234_567.012_345_67e12_345_678" - testCaseAsync "keep sign for exponent" <| - CodeFix.check server + testCaseAsync "keep sign for exponent" + <| CodeFix.check + server "let value = 1234567.01234567e+12345678$0" Diagnostics.acceptAll (CodeFix.withTitle Title.Float.Separate.all3) "let value = 1_234_567.012_345_67e+12_345_678" - testCaseAsync "cannot add separator when existing separator" <| - CodeFix.checkNotApplicable server + testCaseAsync "cannot add separator when existing separator" + <| CodeFix.checkNotApplicable + server "let value = 1234567.0123_4567$0" Diagnostics.acceptAll (CodeFix.withTitle Title.Float.Separate.all3) - testCaseAsync "does not trigger for short number" <| - CodeFix.checkNotApplicable server + testCaseAsync "does not trigger for short number" + <| CodeFix.checkNotApplicable + server "let value = 123.012e123$0" Diagnostics.acceptAll (CodeFix.withTitle Title.Float.Separate.all3) - testCaseAsync "can add separator to just decimal part when other parts are too short" <| - CodeFix.check server + testCaseAsync "can add separator to just decimal part when other parts are too short" + <| CodeFix.check + server "let value = 123.01234567e+123$0" Diagnostics.acceptAll (CodeFix.withTitle Title.Float.Separate.all3) "let value = 123.012_345_67e+123" - testCaseAsync "can add separator to just int part when other parts are too short" <| - CodeFix.check server + testCaseAsync "can add separator to just int part when other parts are too short" + <| CodeFix.check + server "let value = 1234567.012e+123$0" Diagnostics.acceptAll (CodeFix.withTitle Title.Float.Separate.all3) "let value = 1_234_567.012e+123" - testCaseAsync "can add separator to just exponent part when other parts are too short" <| - CodeFix.check server + testCaseAsync "can add separator to just exponent part when other parts are too short" + <| CodeFix.check + server "let value = 123.012e+1234567$0" Diagnostics.acceptAll (CodeFix.withTitle Title.Float.Separate.all3) "let value = 123.012e+1_234_567" - testCaseAsync "can add separator to decimal & exponent parts when int part is too short" <| - CodeFix.check server + testCaseAsync "can add separator to decimal & exponent parts when int part is too short" + <| CodeFix.check + server "let value = 123.012345678e+1234567$0" Diagnostics.acceptAll (CodeFix.withTitle Title.Float.Separate.all3) - "let value = 123.012_345_678e+1_234_567" - ]) + "let value = 123.012_345_678e+1_234_567" ]) - let tests state = - testList "Add Digit Group Separator" [ - intTests state - floatTests state - ] + let tests state = testList "Add Digit Group Separator" [ intTests state; floatTests state ] module private ReplaceWithName = /// Note: `System` is `open` let checkReplaceWith server tyName value fieldName = let replacement = $"{tyName}.{fieldName}" - CodeFix.check server + + CodeFix.check + server $"open System\nlet value = {value}$0" Diagnostics.acceptAll (CodeFix.withTitle (Title.replaceWith replacement)) $"open System\nlet value = {replacement}" + let checkCannotReplaceWith server tyName value fieldName = let replacement = $"{tyName}.{fieldName}" - CodeFix.checkNotApplicable server + + CodeFix.checkNotApplicable + server $"open System\nlet value = {value}$0" Diagnostics.acceptAll (CodeFix.withTitle (Title.replaceWith replacement)) let private intTests state = - serverTestList "Replace Int" state defaultConfigDto None (fun server -> [ - let checkReplaceWith = checkReplaceWith server - let checkCannotReplaceWith = checkCannotReplaceWith server - - /// Formats with suffix - let inline format value = sprintf "%A" value - - testList "can replace SByte" [ - testCaseAsync "with MaxValue" <| - checkReplaceWith (nameof System.SByte) (format SByte.MaxValue) (nameof System.SByte.MaxValue) - testCaseAsync "with MinValue" <| - checkReplaceWith (nameof System.SByte) (format SByte.MinValue) (nameof(System.SByte.MinValue)) - ] - testList "can replace Byte" [ - testCaseAsync "with MaxValue" <| - checkReplaceWith (nameof System.Byte) (format Byte.MaxValue) (nameof System.Byte.MaxValue) - testCaseAsync "not with MinValue" <| - checkCannotReplaceWith (nameof System.Byte) (format Byte.MinValue) (nameof(System.Byte.MinValue)) - ] - testList "can replace Int16" [ - testCaseAsync "with MaxValue" <| - checkReplaceWith (nameof System.Int16) (format Int16.MaxValue) (nameof System.Int16.MaxValue) - testCaseAsync "with MinValue" <| - checkReplaceWith (nameof System.Int16) (format Int16.MinValue) (nameof(System.Int16.MinValue)) - ] - testList "can replace UInt16" [ - testCaseAsync "with MaxValue" <| - checkReplaceWith (nameof System.UInt16) (format UInt16.MaxValue) (nameof System.UInt16.MaxValue) - testCaseAsync "not with MinValue" <| - checkCannotReplaceWith (nameof System.UInt16) (format UInt16.MinValue) (nameof(System.UInt16.MinValue)) - ] - testList "can replace Int32" [ - testCaseAsync "with MaxValue" <| - checkReplaceWith (nameof System.Int32) (format Int32.MaxValue) (nameof System.Int32.MaxValue) - testCaseAsync "with MinValue" <| - checkReplaceWith (nameof System.Int32) (format Int32.MinValue) (nameof(System.Int32.MinValue)) - ] - testList "can replace UInt32" [ - testCaseAsync "with MaxValue" <| - checkReplaceWith (nameof System.UInt32) (format UInt32.MaxValue) (nameof System.UInt32.MaxValue) - testCaseAsync "not with MinValue" <| - checkCannotReplaceWith (nameof System.UInt32) (format UInt32.MinValue) (nameof(System.UInt32.MinValue)) - ] - testList "can replace NativeInt" [ - testCaseAsync "with MaxValue" <| - checkReplaceWith (nameof System.IntPtr) (format IntPtr.MaxValue) (nameof System.IntPtr.MaxValue) - testCaseAsync "with MinValue" <| - checkReplaceWith (nameof System.IntPtr) (format IntPtr.MinValue) (nameof(System.IntPtr.MinValue)) - ] - testList "can replace UNativeInt" [ - testCaseAsync "with MaxValue" <| - checkReplaceWith (nameof System.UIntPtr) (format UIntPtr.MaxValue) (nameof System.UIntPtr.MaxValue) - testCaseAsync "not with MinValue" <| - checkCannotReplaceWith (nameof System.UIntPtr) (format UIntPtr.MinValue) (nameof(System.UIntPtr.MinValue)) - ] - testList "can replace Int64" [ - testCaseAsync "with MaxValue" <| - checkReplaceWith (nameof System.Int64) (format Int64.MaxValue) (nameof System.Int64.MaxValue) - testCaseAsync "with MinValue" <| - checkReplaceWith (nameof System.Int64) (format Int64.MinValue) (nameof(System.Int64.MinValue)) - ] - testList "can replace UInt64" [ - testCaseAsync "with MaxValue" <| - checkReplaceWith (nameof System.UInt64) (format UInt64.MaxValue) (nameof System.UInt64.MaxValue) - testCaseAsync "not with MinValue" <| - checkCannotReplaceWith (nameof System.UInt64) (format UInt64.MinValue) (nameof(System.UInt64.MinValue)) - ] - - testCaseAsync "Emit leading System if System not open" <| - CodeFix.check server + serverTestList "Replace Int" state defaultConfigDto None (fun server -> + [ let checkReplaceWith = checkReplaceWith server + let checkCannotReplaceWith = checkCannotReplaceWith server + + /// Formats with suffix + let inline format value = sprintf "%A" value + + testList + "can replace SByte" + [ testCaseAsync "with MaxValue" + <| checkReplaceWith (nameof System.SByte) (format SByte.MaxValue) (nameof System.SByte.MaxValue) + testCaseAsync "with MinValue" + <| checkReplaceWith (nameof System.SByte) (format SByte.MinValue) (nameof (System.SByte.MinValue)) ] + + testList + "can replace Byte" + [ testCaseAsync "with MaxValue" + <| checkReplaceWith (nameof System.Byte) (format Byte.MaxValue) (nameof System.Byte.MaxValue) + testCaseAsync "not with MinValue" + <| checkCannotReplaceWith (nameof System.Byte) (format Byte.MinValue) (nameof (System.Byte.MinValue)) ] + + testList + "can replace Int16" + [ testCaseAsync "with MaxValue" + <| checkReplaceWith (nameof System.Int16) (format Int16.MaxValue) (nameof System.Int16.MaxValue) + testCaseAsync "with MinValue" + <| checkReplaceWith (nameof System.Int16) (format Int16.MinValue) (nameof (System.Int16.MinValue)) ] + + testList + "can replace UInt16" + [ testCaseAsync "with MaxValue" + <| checkReplaceWith (nameof System.UInt16) (format UInt16.MaxValue) (nameof System.UInt16.MaxValue) + testCaseAsync "not with MinValue" + <| checkCannotReplaceWith (nameof System.UInt16) (format UInt16.MinValue) (nameof (System.UInt16.MinValue)) ] + + testList + "can replace Int32" + [ testCaseAsync "with MaxValue" + <| checkReplaceWith (nameof System.Int32) (format Int32.MaxValue) (nameof System.Int32.MaxValue) + testCaseAsync "with MinValue" + <| checkReplaceWith (nameof System.Int32) (format Int32.MinValue) (nameof (System.Int32.MinValue)) ] + + testList + "can replace UInt32" + [ testCaseAsync "with MaxValue" + <| checkReplaceWith (nameof System.UInt32) (format UInt32.MaxValue) (nameof System.UInt32.MaxValue) + testCaseAsync "not with MinValue" + <| checkCannotReplaceWith (nameof System.UInt32) (format UInt32.MinValue) (nameof (System.UInt32.MinValue)) ] + + testList + "can replace NativeInt" + [ testCaseAsync "with MaxValue" + <| checkReplaceWith (nameof System.IntPtr) (format IntPtr.MaxValue) (nameof System.IntPtr.MaxValue) + testCaseAsync "with MinValue" + <| checkReplaceWith (nameof System.IntPtr) (format IntPtr.MinValue) (nameof (System.IntPtr.MinValue)) ] + + testList + "can replace UNativeInt" + [ testCaseAsync "with MaxValue" + <| checkReplaceWith (nameof System.UIntPtr) (format UIntPtr.MaxValue) (nameof System.UIntPtr.MaxValue) + testCaseAsync "not with MinValue" + <| checkCannotReplaceWith + (nameof System.UIntPtr) + (format UIntPtr.MinValue) + (nameof (System.UIntPtr.MinValue)) ] + + testList + "can replace Int64" + [ testCaseAsync "with MaxValue" + <| checkReplaceWith (nameof System.Int64) (format Int64.MaxValue) (nameof System.Int64.MaxValue) + testCaseAsync "with MinValue" + <| checkReplaceWith (nameof System.Int64) (format Int64.MinValue) (nameof (System.Int64.MinValue)) ] + + testList + "can replace UInt64" + [ testCaseAsync "with MaxValue" + <| checkReplaceWith (nameof System.UInt64) (format UInt64.MaxValue) (nameof System.UInt64.MaxValue) + testCaseAsync "not with MinValue" + <| checkCannotReplaceWith (nameof System.UInt64) (format UInt64.MinValue) (nameof (System.UInt64.MinValue)) ] + + testCaseAsync "Emit leading System if System not open" + <| CodeFix.check + server $"let value = {format Int32.MaxValue}$0" Diagnostics.acceptAll (CodeFix.withTitle (Title.replaceWith "Int32.MaxValue")) - $"let value = System.Int32.MaxValue" - ]) + $"let value = System.Int32.MaxValue" ]) let private floatTests state = - serverTestList "Replace Float" state defaultConfigDto None (fun server -> [ - // Beware of rounding in number printing! - // For example: - // ```fsharp - // > Double.MaxValue;; - // val it: float = 1.797693135e+308 - // > 1.797693135e+308;; - // val it: float = infinity - - // > Double.MaxValue.ToString();; - // val it: string = "1.7976931348623157E+308" - // ``` - - let checkReplaceWith = checkReplaceWith server - let checkCannotReplaceWith = checkCannotReplaceWith server - let checkReplaceWith' value name = - CodeFix.check server - $"let value = {value}$0" - Diagnostics.acceptAll - (CodeFix.withTitle (Title.replaceWith name)) - $"let value = {name}" - - testList "can replace float" [ - testCaseAsync "with MaxValue" <| - checkReplaceWith (nameof System.Double) "1.7976931348623157E+308" (nameof System.Double.MaxValue) - testCaseAsync "with MinValue" <| - checkReplaceWith (nameof System.Double) "-1.7976931348623157E+308" (nameof System.Double.MinValue) - testCaseAsync "with Epsilon" <| - checkReplaceWith (nameof System.Double) "5E-324" (nameof System.Double.Epsilon) - testCaseAsync "with infinity" <| - checkReplaceWith' "123456789e123456789" "infinity" - testCaseAsync "with infinity (int)" <| - checkReplaceWith' "0x7FF0000000000000LF" "infinity" - testCaseAsync "with -infinity" <| - checkReplaceWith' "-123456789e123456789" "-infinity" - testCaseAsync "with -infinity (int)" <| - checkReplaceWith' "0o1777600000000000000000LF" "-infinity" - testCaseAsync "with nan (int)" <| - checkReplaceWith' "0b1111111111111000000100010001010010010010001000100010001000100100LF" "nan" - ] - testList "can replace float32" [ - testCaseAsync "with MaxValue" <| - checkReplaceWith (nameof System.Single) "3.4028235E+38f" (nameof System.Single.MaxValue) - testCaseAsync "with MinValue" <| - checkReplaceWith (nameof System.Single) "-3.4028235E+38f" (nameof System.Single.MinValue) - testCaseAsync "with Epsilon" <| - checkReplaceWith (nameof System.Single) "1.401298464e-45f" (nameof System.Single.Epsilon) - testCaseAsync "with infinity" <| - checkReplaceWith' "123456789e123456789f" "infinityf" - testCaseAsync "with infinity (int)" <| - checkReplaceWith' "0x7F800000lf" "infinityf" - testCaseAsync "with -infinity" <| - checkReplaceWith' "-123456789e123456789f" "-infinityf" - testCaseAsync "with -infinity (int)" <| - checkReplaceWith' "0o37740000000lf" "-infinityf" - testCaseAsync "with nan (int)" <| - checkReplaceWith' "0b1111111101001000100100100100100lf" "nanf" - ] - - testCaseAsync "Emit leading System if System not open" <| - CodeFix.check server + serverTestList "Replace Float" state defaultConfigDto None (fun server -> + [ + // Beware of rounding in number printing! + // For example: + // ```fsharp + // > Double.MaxValue;; + // val it: float = 1.797693135e+308 + // > 1.797693135e+308;; + // val it: float = infinity + + // > Double.MaxValue.ToString();; + // val it: string = "1.7976931348623157E+308" + // ``` + + let checkReplaceWith = checkReplaceWith server + let _checkCannotReplaceWith = checkCannotReplaceWith server + + let checkReplaceWith' value name = + CodeFix.check + server + $"let value = {value}$0" + Diagnostics.acceptAll + (CodeFix.withTitle (Title.replaceWith name)) + $"let value = {name}" + + testList + "can replace float" + [ testCaseAsync "with MaxValue" + <| checkReplaceWith (nameof System.Double) "1.7976931348623157E+308" (nameof System.Double.MaxValue) + testCaseAsync "with MinValue" + <| checkReplaceWith (nameof System.Double) "-1.7976931348623157E+308" (nameof System.Double.MinValue) + testCaseAsync "with Epsilon" + <| checkReplaceWith (nameof System.Double) "5E-324" (nameof System.Double.Epsilon) + testCaseAsync "with infinity" + <| checkReplaceWith' "123456789e123456789" "infinity" + testCaseAsync "with infinity (int)" + <| checkReplaceWith' "0x7FF0000000000000LF" "infinity" + testCaseAsync "with -infinity" + <| checkReplaceWith' "-123456789e123456789" "-infinity" + testCaseAsync "with -infinity (int)" + <| checkReplaceWith' "0o1777600000000000000000LF" "-infinity" + testCaseAsync "with nan (int)" + <| checkReplaceWith' "0b1111111111111000000100010001010010010010001000100010001000100100LF" "nan" ] + + testList + "can replace float32" + [ testCaseAsync "with MaxValue" + <| checkReplaceWith (nameof System.Single) "3.4028235E+38f" (nameof System.Single.MaxValue) + testCaseAsync "with MinValue" + <| checkReplaceWith (nameof System.Single) "-3.4028235E+38f" (nameof System.Single.MinValue) + testCaseAsync "with Epsilon" + <| checkReplaceWith (nameof System.Single) "1.401298464e-45f" (nameof System.Single.Epsilon) + testCaseAsync "with infinity" + <| checkReplaceWith' "123456789e123456789f" "infinityf" + testCaseAsync "with infinity (int)" + <| checkReplaceWith' "0x7F800000lf" "infinityf" + testCaseAsync "with -infinity" + <| checkReplaceWith' "-123456789e123456789f" "-infinityf" + testCaseAsync "with -infinity (int)" + <| checkReplaceWith' "0o37740000000lf" "-infinityf" + testCaseAsync "with nan (int)" + <| checkReplaceWith' "0b1111111101001000100100100100100lf" "nanf" ] + + testCaseAsync "Emit leading System if System not open" + <| CodeFix.check + server $"let value = 1.7976931348623157E+308$0" Diagnostics.acceptAll (CodeFix.withTitle (Title.replaceWith "Double.MaxValue")) $"let value = System.Double.MaxValue" - testList "can replace decimal" [ - testCaseAsync "with MaxValue" <| - checkReplaceWith (nameof System.Decimal) "79228162514264337593543950335m" (nameof System.Decimal.MaxValue) - testCaseAsync "with MinValue" <| - checkReplaceWith (nameof System.Decimal) "-79228162514264337593543950335m" (nameof System.Decimal.MinValue) - ] + testList + "can replace decimal" + [ testCaseAsync "with MaxValue" + <| checkReplaceWith + (nameof System.Decimal) + "79228162514264337593543950335m" + (nameof System.Decimal.MaxValue) + testCaseAsync "with MinValue" + <| checkReplaceWith + (nameof System.Decimal) + "-79228162514264337593543950335m" + (nameof System.Decimal.MinValue) ] - ]) - let tests state = - testList "Replace With Name" [ - intTests state - floatTests state - ] + ]) + + let tests state = testList "Replace With Name" [ intTests state; floatTests state ] module SignHelpers = let tests state = - serverTestList "Sign Helpers" state defaultConfigDto None (fun server -> [ - testList "extract `-`" [ - testCaseAsync "from bin int" <| - CodeFix.check server - "let value = 0b10000101y$0" - Diagnostics.acceptAll - (CodeFix.withTitle Title.Int.Convert.SpecialCase.extractMinusFromNegativeConstant) - "let value = -0b1111011y" - testCaseAsync "from hex int" <| - CodeFix.check server - "let value = 0x85y$0" - Diagnostics.acceptAll - (CodeFix.withTitle Title.Int.Convert.SpecialCase.extractMinusFromNegativeConstant) - "let value = -0x7By" - testCaseAsync "from oct int" <| - CodeFix.check server - "let value = 0o205y$0" - Diagnostics.acceptAll - (CodeFix.withTitle Title.Int.Convert.SpecialCase.extractMinusFromNegativeConstant) - "let value = -0o173y" - testCaseAsync "does not trigger for decimal int" <| - CodeFix.checkNotApplicable server - "let value = -123y$0" - Diagnostics.acceptAll - (CodeFix.withTitle Title.Int.Convert.SpecialCase.extractMinusFromNegativeConstant) - ] - testList "integrate `-`" [ - testCaseAsync "into bin int" <| - CodeFix.check server - "let value = -0b1111011y$0" - Diagnostics.acceptAll - (CodeFix.withTitle Title.Int.Convert.SpecialCase.integrateExplicitMinus) - "let value = 0b10000101y" - testCaseAsync "into hex int" <| - CodeFix.check server - "let value = -0x7By$0" - Diagnostics.acceptAll - (CodeFix.withTitle Title.Int.Convert.SpecialCase.integrateExplicitMinus) - "let value = 0x85y" - testCaseAsync "into oct int" <| - CodeFix.check server - "let value = -0o173y$0" - Diagnostics.acceptAll - (CodeFix.withTitle Title.Int.Convert.SpecialCase.integrateExplicitMinus) - "let value = 0o205y" - testCaseAsync "does not trigger for decimal int" <| - CodeFix.checkNotApplicable server - "let value = -123y$0" - Diagnostics.acceptAll - (CodeFix.withTitle Title.Int.Convert.SpecialCase.integrateExplicitMinus) - ] - - testList "MinValue" [ - testCaseAsync "can remove explicit `-`" <| - CodeFix.check server - "let value = -0b10000000y$0" - Diagnostics.acceptAll - (CodeFix.withTitle Title.Int.Convert.SpecialCase.removeExplicitMinusWithMinValue) - "let value = 0b10000000y" - testCaseAsync "does not trigger for decimal int" <| - CodeFix.checkNotApplicable server - "let value = -127y$0" - Diagnostics.acceptAll - (CodeFix.withTitle Title.Int.Convert.SpecialCase.removeExplicitMinusWithMinValue) - ] - - testList "use implicit `+`" [ - testCaseAsync "can change to positive" <| - CodeFix.check server - "let value = -0b1111_1101y$0" - Diagnostics.acceptAll - (CodeFix.withTitle Title.Int.Convert.SpecialCase.useImplicitPlusInPositiveConstantWithMinusSign) - "let value = 0b11y" - ] - - testList "ensure valid sign" [ - // QuickFixes might add sign which might lead to invalid code: - // ```fsharp - // // -91y - // let value = 5y+0b1010_0101y - - // // => Convert to decimal + serverTestList "Sign Helpers" state defaultConfigDto None (fun server -> + [ testList + "extract `-`" + [ testCaseAsync "from bin int" + <| CodeFix.check + server + "let value = 0b10000101y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.SpecialCase.extractMinusFromNegativeConstant) + "let value = -0b1111011y" + testCaseAsync "from hex int" + <| CodeFix.check + server + "let value = 0x85y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.SpecialCase.extractMinusFromNegativeConstant) + "let value = -0x7By" + testCaseAsync "from oct int" + <| CodeFix.check + server + "let value = 0o205y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.SpecialCase.extractMinusFromNegativeConstant) + "let value = -0o173y" + testCaseAsync "does not trigger for decimal int" + <| CodeFix.checkNotApplicable + server + "let value = -123y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.SpecialCase.extractMinusFromNegativeConstant) ] + testList + "integrate `-`" + [ testCaseAsync "into bin int" + <| CodeFix.check + server + "let value = -0b1111011y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.SpecialCase.integrateExplicitMinus) + "let value = 0b10000101y" + testCaseAsync "into hex int" + <| CodeFix.check + server + "let value = -0x7By$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.SpecialCase.integrateExplicitMinus) + "let value = 0x85y" + testCaseAsync "into oct int" + <| CodeFix.check + server + "let value = -0o173y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.SpecialCase.integrateExplicitMinus) + "let value = 0o205y" + testCaseAsync "does not trigger for decimal int" + <| CodeFix.checkNotApplicable + server + "let value = -123y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.SpecialCase.integrateExplicitMinus) ] + + testList + "MinValue" + [ testCaseAsync "can remove explicit `-`" + <| CodeFix.check + server + "let value = -0b10000000y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.SpecialCase.removeExplicitMinusWithMinValue) + "let value = 0b10000000y" + testCaseAsync "does not trigger for decimal int" + <| CodeFix.checkNotApplicable + server + "let value = -127y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.SpecialCase.removeExplicitMinusWithMinValue) ] + + testList + "use implicit `+`" + [ testCaseAsync "can change to positive" + <| CodeFix.check + server + "let value = -0b1111_1101y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.SpecialCase.useImplicitPlusInPositiveConstantWithMinusSign) + "let value = 0b11y" ] + + testList + "ensure valid sign" + [ + // QuickFixes might add sign which might lead to invalid code: + // ```fsharp + // // -91y + // let value = 5y+0b1010_0101y + + // // => Convert to decimal + + // let value = 5y+-91y + // // ^^ + // // The type 'sbyte' does not support the operator '+-' + // ``` + // + // -> insert space before sign if necessary + + testCaseAsync "add space when new `-` sign immediately after `+`" + <| CodeFix.check + server + "let value = 5y+0b1010_0101y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.toDecimal) + "let value = 5y+ -91y" + testCaseAsync "don't add space when `-` with space before" + <| CodeFix.check + server + "let value = 5y+ 0b1010_0101y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.toDecimal) + "let value = 5y+ -91y" + testCaseAsync "don't add space when new `-` sign immediately after `(`" + <| CodeFix.check + server + "let value = 5y+(0b1010_0101y$0)" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.toDecimal) + "let value = 5y+(-91y)" + testCaseAsync "add space when new `-` sign immediately after `<|`" + <| CodeFix.check + server + "let value = max 5y <|0b1010_0101y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.toDecimal) + "let value = max 5y <| -91y" + testCaseAsync "don't add space when no new `-` sign" + <| CodeFix.check + server + "let value = 5y+0b1011011y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.toDecimal) + "let value = 5y+91y" - // let value = 5y+-91y - // // ^^ - // // The type 'sbyte' does not support the operator '+-' - // ``` - // - // -> insert space before sign if necessary + testCaseAsync "add space when convert to other base" + <| CodeFix.check + server + "let value = 5y+0b1010_0101y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.toDecimal) + "let value = 5y+ -91y" + testCaseAsync "add space when extract `-`" + <| CodeFix.check + server + "let value = 5y+0b10000101y$0" + Diagnostics.acceptAll + (CodeFix.withTitle Title.Int.Convert.SpecialCase.extractMinusFromNegativeConstant) + "let value = 5y+ -0b1111011y" - testCaseAsync "add space when new `-` sign immediately after `+`" <| - CodeFix.check server - "let value = 5y+0b1010_0101y$0" - Diagnostics.acceptAll - (CodeFix.withTitle Title.Int.Convert.toDecimal) - "let value = 5y+ -91y" - testCaseAsync "don't add space when `-` with space before" <| - CodeFix.check server - "let value = 5y+ 0b1010_0101y$0" - Diagnostics.acceptAll - (CodeFix.withTitle Title.Int.Convert.toDecimal) - "let value = 5y+ -91y" - testCaseAsync "don't add space when new `-` sign immediately after `(`" <| - CodeFix.check server - "let value = 5y+(0b1010_0101y$0)" - Diagnostics.acceptAll - (CodeFix.withTitle Title.Int.Convert.toDecimal) - "let value = 5y+(-91y)" - testCaseAsync "add space when new `-` sign immediately after `<|`" <| - CodeFix.check server - "let value = max 5y <|0b1010_0101y$0" - Diagnostics.acceptAll - (CodeFix.withTitle Title.Int.Convert.toDecimal) - "let value = max 5y <| -91y" - testCaseAsync "don't add space when no new `-` sign" <| - CodeFix.check server - "let value = 5y+0b1011011y$0" - Diagnostics.acceptAll - (CodeFix.withTitle Title.Int.Convert.toDecimal) - "let value = 5y+91y" + testCaseAsync "add space when convert to `-infinity`" + <| CodeFix.check + server + "let value = 5.0+0o1777600000000000000000LF$0" + Diagnostics.acceptAll + (CodeFix.withTitle (Title.replaceWith "-infinity")) + "let value = 5.0+ -infinity" ] ]) - testCaseAsync "add space when convert to other base" <| - CodeFix.check server - "let value = 5y+0b1010_0101y$0" - Diagnostics.acceptAll - (CodeFix.withTitle Title.Int.Convert.toDecimal) - "let value = 5y+ -91y" - testCaseAsync "add space when extract `-`" <| - CodeFix.check server - "let value = 5y+0b10000101y$0" - Diagnostics.acceptAll - (CodeFix.withTitle Title.Int.Convert.SpecialCase.extractMinusFromNegativeConstant) - "let value = 5y+ -0b1111011y" +let tests state = + testList + (nameof AdjustConstant) + [ ConvertIntToOtherBase.tests state + ConvertIntToOtherBase.Float.tests state + ConvertCharToOtherForm.tests state + ConvertByteBetweenIntAndChar.tests state - testCaseAsync "add space when convert to `-infinity`" <| - CodeFix.check server - "let value = 5.0+0o1777600000000000000000LF$0" - Diagnostics.acceptAll - (CodeFix.withTitle (Title.replaceWith "-infinity")) - "let value = 5.0+ -infinity" - ] - ]) + ReplaceWithName.tests state + SignHelpers.tests state -let tests state = - testList (nameof AdjustConstant) [ - ConvertIntToOtherBase.tests state - ConvertIntToOtherBase.Float.tests state - ConvertCharToOtherForm.tests state - ConvertByteBetweenIntAndChar.tests state - - ReplaceWithName.tests state - SignHelpers.tests state - - AddDigitGroupSeparator.tests state - ] + AddDigitGroupSeparator.tests state ] diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/RenameParamToMatchSignatureTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/RenameParamToMatchSignatureTests.fs index daa72bc34..e4f01db14 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/RenameParamToMatchSignatureTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/RenameParamToMatchSignatureTests.fs @@ -18,38 +18,37 @@ let tests state = // requires `fsi` and corresponding `fs` file (and a project!) // -> cannot use untitled doc // -> use existing files, but load with text specified in tests - let path = Path.Combine(__SOURCE_DIRECTORY__, @"../TestCases/CodeFixTests/RenameParamToMatchSignature/") + let path = + Path.Combine(__SOURCE_DIRECTORY__, @"../TestCases/CodeFixTests/RenameParamToMatchSignature/") + let (fsiFile, fsFile) = ("Code.fsi", "Code.fs") - serverTestList (nameof RenameParamToMatchSignature) state defaultConfigDto (Some path) (fun server -> [ - let checkWithFsi - fsiSource - fsSourceWithCursor - selectCodeFix - fsSourceExpected - = async { - let fsiSource = fsiSource |> Text.trimTripleQuotation - let (cursor, fsSource) = - fsSourceWithCursor - |> Text.trimTripleQuotation - |> Cursor.assertExtractRange - let! (fsiDoc, diags) = server |> Server.openDocumentWithText fsiFile fsiSource - use fsiDoc = fsiDoc - Expect.isEmpty diags "There should be no diagnostics in fsi doc" - let! (fsDoc, diags) = server |> Server.openDocumentWithText fsFile fsSource - use fsDoc = fsDoc - - do! - checkFixAt - (fsDoc, diags) - (fsSource, cursor) - (Diagnostics.expectCode "3218") - selectCodeFix - (After (fsSourceExpected |> Text.trimTripleQuotation)) - } - - testCaseAsync "can rename parameter in F# function" <| - checkWithFsi + serverTestList (nameof RenameParamToMatchSignature) state defaultConfigDto (Some path) (fun server -> + [ let checkWithFsi fsiSource fsSourceWithCursor selectCodeFix fsSourceExpected = + async { + let fsiSource = fsiSource |> Text.trimTripleQuotation + + let (cursor, fsSource) = + fsSourceWithCursor |> Text.trimTripleQuotation |> Cursor.assertExtractRange + + let! (fsiDoc, diags) = server |> Server.openDocumentWithText fsiFile fsiSource + use _fsiDoc = fsiDoc + Expect.isEmpty diags "There should be no diagnostics in fsi doc" + let! (fsDoc, diags) = server |> Server.openDocumentWithText fsFile fsSource + use fsDoc = fsDoc + + do! + checkFixAt + (fsDoc, diags) + fsDoc.VersionedTextDocumentIdentifier + (fsSource, cursor) + (Diagnostics.expectCode "3218") + selectCodeFix + (After(fsSourceExpected |> Text.trimTripleQuotation)) + } + + testCaseAsync "can rename parameter in F# function" + <| checkWithFsi """ module Code @@ -66,8 +65,9 @@ let tests state = let f value1 = value1 + 1 """ - testCaseAsync "can rename parameter with backticks in signature in F# function" <| - checkWithFsi + + testCaseAsync "can rename parameter with backticks in signature in F# function" + <| checkWithFsi """ module Code @@ -84,8 +84,9 @@ let tests state = let f ``my value2`` = ``my value2`` + 1 """ - testCaseAsync "can rename parameter with backticks in implementation in F# function" <| - checkWithFsi + + testCaseAsync "can rename parameter with backticks in implementation in F# function" + <| checkWithFsi """ module Code @@ -102,8 +103,9 @@ let tests state = let f value3 = value3 + 1 """ - testCaseAsync "can rename all usage in F# function" <| - checkWithFsi + + testCaseAsync "can rename all usage in F# function" + <| checkWithFsi """ module Code @@ -128,8 +130,9 @@ let tests state = let v = a + b v + x * y """ - testCaseAsync "can rename parameter with type in F# function" <| - checkWithFsi + + testCaseAsync "can rename parameter with type in F# function" + <| checkWithFsi """ module Code @@ -146,8 +149,9 @@ let tests state = let f (value5: int) = value5 + 1 """ - testCaseAsync "can rename parameter in constructor" <| - checkWithFsi + + testCaseAsync "can rename parameter in constructor" + <| checkWithFsi """ module Code @@ -167,8 +171,9 @@ let tests state = type T(value6: int) = let _ = value6 + 3 """ - testCaseAsync "can rename parameter in member" <| - checkWithFsi + + testCaseAsync "can rename parameter in member" + <| checkWithFsi """ module Code @@ -189,8 +194,9 @@ let tests state = type T() = member _.F(value7) = value7 + 1 """ - testCaseAsync "can rename parameter with ' in signature in F# function" <| - checkWithFsi + + testCaseAsync "can rename parameter with ' in signature in F# function" + <| checkWithFsi """ module Code @@ -207,8 +213,9 @@ let tests state = let f value8' = value8' + 1 """ - testCaseAsync "can rename parameter with ' in implementation in F# function" <| - checkWithFsi + + testCaseAsync "can rename parameter with ' in implementation in F# function" + <| checkWithFsi """ module Code @@ -225,8 +232,9 @@ let tests state = let f value9 = value9 + 1 """ - testCaseAsync "can rename parameter with ' (not in last place) in signature in F# function" <| - checkWithFsi + + testCaseAsync "can rename parameter with ' (not in last place) in signature in F# function" + <| checkWithFsi """ module Code @@ -243,8 +251,9 @@ let tests state = let f v10'2 = v10'2 + 1 """ - testCaseAsync "can rename parameter with ' (not in last place) in implementation in F# function" <| - checkWithFsi + + testCaseAsync "can rename parameter with ' (not in last place) in implementation in F# function" + <| checkWithFsi """ module Code @@ -261,8 +270,9 @@ let tests state = let f value11 = value11 + 1 """ - testCaseAsync "can rename parameter with multiple ' in signature in F# function" <| - checkWithFsi + + testCaseAsync "can rename parameter with multiple ' in signature in F# function" + <| checkWithFsi """ module Code @@ -279,8 +289,9 @@ let tests state = let f value12'v'2 = value12'v'2 + 1 """ - testCaseAsync "can rename parameter with multiple ' in implementation in F# function" <| - checkWithFsi + + testCaseAsync "can rename parameter with multiple ' in implementation in F# function" + <| checkWithFsi """ module Code @@ -297,8 +308,9 @@ let tests state = let f value13 = value13 + 1 """ - itestCaseAsync "can handle `' and implementation '` in impl name" <| - checkWithFsi + + itestCaseAsync "can handle `' and implementation '` in impl name" + <| checkWithFsi """ module Code @@ -315,9 +327,9 @@ let tests state = let f value14 = value14 + 1 """ - //ENHANCEMENT: correctly detect below. Currently: detects sig name `sig` - itestCaseAsync "can handle `' and implementation '` in sig name" <| - checkWithFsi + //ENHANCEMENT: correctly detect below. Currently: detects sig name `sig` + itestCaseAsync "can handle `' and implementation '` in sig name" + <| checkWithFsi """ module Code @@ -333,5 +345,4 @@ let tests state = module Code let f ``sig' and implementation 'impl' do not match`` = ``sig' and implementation 'impl' do not match`` + 1 - """ - ]) + """ ]) diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs index 3e1115ba8..dfd80ea5a 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs @@ -1,5 +1,6 @@ module FsAutoComplete.Tests.CodeFixTests.Tests +open System open Expecto open Helpers open Utils.ServerTests @@ -2053,7 +2054,7 @@ let private generateUnionCasesTests state = UnionCaseStubGenerationBody = Some "failwith \"---\"" } serverTestList (nameof GenerateUnionCases) state config None (fun server -> - [ let selectCodeFix = CodeFix.withTitle GenerateUnionCases.title + [ let _selectCodeFix = CodeFix.withTitle GenerateUnionCases.title testCaseAsync "can generate match cases for a simple DU" <| CodeFix.check @@ -2674,8 +2675,7 @@ let private renameUnusedValue state = let private replaceWithSuggestionTests state = serverTestList (nameof ReplaceWithSuggestion) state defaultConfigDto None (fun server -> - [ let selectCodeFix replacement = - CodeFix.withTitle (ReplaceWithSuggestion.title replacement) + [ let selectCodeFix replacement = CodeFix.withTitle (ReplaceWithSuggestion.title replacement) let validateDiags (diags: Diagnostic[]) = Diagnostics.expectCode "39" diags @@ -2794,7 +2794,7 @@ let private resolveNamespaceTests state = ResolveNamespaces = Some true } serverTestList (nameof ResolveNamespace) state config None (fun server -> - [ let selectCodeFix = CodeFix.matching (fun ca -> ca.Title.StartsWith "open") + [ let selectCodeFix = CodeFix.matching (fun ca -> ca.Title.StartsWith("open", StringComparison.Ordinal)) testCaseAsync "doesn't fail when target not in last line" <| CodeFix.checkApplicable @@ -3304,50 +3304,52 @@ let private removePatternArgumentTests state = let (None) = None """ ]) -let tests textFactory state = testList "CodeFix-tests" [ - HelpersTests.tests textFactory - AddExplicitTypeAnnotationTests.tests state - AdjustConstantTests.tests state - ToInterpolatedStringTests.tests state - ToInterpolatedStringTests.unavailableTests state - addMissingEqualsToTypeDefinitionTests state - addMissingFunKeywordTests state - addMissingInstanceMemberTests state - addMissingRecKeywordTests state - addMissingXmlDocumentationTests state - addNewKeywordToDisposableConstructorInvocationTests state - addTypeToIndeterminateValueTests state - changeDerefBangToValueTests state - changeDowncastToUpcastTests state - changeEqualsInFieldTypeToColonTests state - changePrefixNegationToInfixSubtractionTests state - changeRefCellDerefToNotTests state - changeTypeOfNameToNameOfTests state - convertBangEqualsToInequalityTests state - convertCSharpLambdaToFSharpLambdaTests state - convertDoubleEqualsToSingleEqualsTests state - convertInvalidRecordToAnonRecordTests state - convertPositionalDUToNamedTests state - convertTripleSlashCommentToXmlTaggedDocTests state - addPrivateAccessModifierTests state - GenerateAbstractClassStubTests.tests state - generateRecordStubTests state - generateUnionCasesTests state - generateXmlDocumentationTests state - ImplementInterfaceTests.tests state - makeDeclarationMutableTests state - makeOuterBindingRecursiveTests state - removeRedundantQualifierTests state - removeUnnecessaryReturnOrYieldTests state - removeUnusedBindingTests state - removeUnusedOpensTests state - RenameParamToMatchSignatureTests.tests state - renameUnusedValue state - replaceWithSuggestionTests state - resolveNamespaceTests state - useMutationWhenValueIsMutableTests state - useTripleQuotedInterpolationTests state - wrapExpressionInParenthesesTests state - removeRedundantAttributeSuffixTests state - removePatternArgumentTests state -] +let tests textFactory state = + testList + "CodeFix-tests" + [ HelpersTests.tests textFactory + AddExplicitTypeAnnotationTests.tests state + AdjustConstantTests.tests state + ToInterpolatedStringTests.tests state + ToInterpolatedStringTests.unavailableTests state + addMissingEqualsToTypeDefinitionTests state + addMissingFunKeywordTests state + addMissingInstanceMemberTests state + addMissingRecKeywordTests state + addMissingXmlDocumentationTests state + addNewKeywordToDisposableConstructorInvocationTests state + addTypeToIndeterminateValueTests state + changeDerefBangToValueTests state + changeDowncastToUpcastTests state + changeEqualsInFieldTypeToColonTests state + changePrefixNegationToInfixSubtractionTests state + changeRefCellDerefToNotTests state + changeTypeOfNameToNameOfTests state + convertBangEqualsToInequalityTests state + convertCSharpLambdaToFSharpLambdaTests state + convertDoubleEqualsToSingleEqualsTests state + convertInvalidRecordToAnonRecordTests state + convertPositionalDUToNamedTests state + convertTripleSlashCommentToXmlTaggedDocTests state + addPrivateAccessModifierTests state + GenerateAbstractClassStubTests.tests state + generateRecordStubTests state + generateUnionCasesTests state + generateXmlDocumentationTests state + ImplementInterfaceTests.tests state + makeDeclarationMutableTests state + makeOuterBindingRecursiveTests state + removeRedundantQualifierTests state + removeUnnecessaryReturnOrYieldTests state + removeUnusedBindingTests state + removeUnusedOpensTests state + RenameParamToMatchSignatureTests.tests state + renameUnusedValue state + replaceWithSuggestionTests state + resolveNamespaceTests state + useMutationWhenValueIsMutableTests state + useTripleQuotedInterpolationTests state + wrapExpressionInParenthesesTests state + removeRedundantAttributeSuffixTests state + removePatternArgumentTests state + UpdateValueInSignatureFileTests.tests state ] diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/UpdateValueInSignatureFileTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/UpdateValueInSignatureFileTests.fs new file mode 100644 index 000000000..4b3a19273 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/UpdateValueInSignatureFileTests.fs @@ -0,0 +1,68 @@ +module private FsAutoComplete.Tests.CodeFixTests.UpdateValueInSignatureFileTests + +open System.IO +open Expecto +open Helpers +open Utils.ServerTests +open Utils.CursorbasedTests +open FsAutoComplete.CodeFix +open Utils.Utils +open Utils.TextEdit +open Utils.Server +open Utils.CursorbasedTests.CodeFix + +let path = Path.Combine(__SOURCE_DIRECTORY__, @"../TestCases/CodeFixTests/RenameParamToMatchSignature/") +let fsiFile, fsFile = ("Code.fsi", "Code.fs") + +let checkWithFsi + server + fsiSource + fsSourceWithCursor + selectCodeFix + fsiSourceExpected + = async { + let fsiSource = fsiSource |> Text.trimTripleQuotation + let cursor, fsSource = + fsSourceWithCursor + |> Text.trimTripleQuotation + |> Cursor.assertExtractRange + let! fsiDoc, diags = server |> Server.openDocumentWithText fsiFile fsiSource + use fsiDoc = fsiDoc + Expect.isEmpty diags "There should be no diagnostics in fsi doc" + let! fsDoc, diags = server |> Server.openDocumentWithText fsFile fsSource + use fsDoc = fsDoc + + do! + checkFixAt + (fsDoc, diags) + fsiDoc.VersionedTextDocumentIdentifier + (fsiSource, cursor) + (Diagnostics.expectCode "34") + selectCodeFix + (After (fsiSourceExpected |> Text.trimTripleQuotation)) + } + +let tests state = + serverTestList (nameof UpdateValueInSignatureFile) state defaultConfigDto (Some path) (fun server -> + [ let selectCodeFix = CodeFix.withTitle UpdateValueInSignatureFile.title + + testCaseAsync "first unit test for UpdateValueInSignatureFile" + <| checkWithFsi + server + """ +module A + +val a: b:int -> int +""" +""" +module A + +let a$0 (b:int) (c: string) = 0 +""" + selectCodeFix + """ +module A + +val a: b: int -> c: string -> int +""" + ]) diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Utils.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Utils.fs index b72087ee3..7ab75a18e 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Utils.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Utils.fs @@ -6,13 +6,16 @@ open FsAutoComplete.Logging module Diagnostics = let expectCode code (diags: Diagnostic[]) = - Expecto.Flip.Expect.exists + Expecto.Flip.Expect.exists $"There should be a Diagnostic with code %s{code}" (fun (d: Diagnostic) -> d.Code = Some code) diags + let acceptAll = ignore - let private logger = FsAutoComplete.Logging.LogProvider.getLoggerByName "CodeFixes.Diagnostics" + let private logger = + FsAutoComplete.Logging.LogProvider.getLoggerByName "CodeFixes.Diagnostics" + /// Usage: `(Diagnostics.log >> Diagnostics.expectCode "XXX")` /// Logs as `info` let log (diags: Diagnostic[]) = @@ -21,10 +24,13 @@ module Diagnostics = >> Log.addContext "count" diags.Length >> Log.addContextDestructured "diags" diags ) + diags module CodeFix = - let private logger = FsAutoComplete.Logging.LogProvider.getLoggerByName "CodeFixes.CodeFix" + let private logger = + FsAutoComplete.Logging.LogProvider.getLoggerByName "CodeFixes.CodeFix" + /// Usage: `(CodeFix.log >> CodeFix.withTitle "XXX")` /// Logs as `info` let log (codeActions: CodeAction[]) = @@ -33,14 +39,15 @@ module CodeFix = >> Log.addContext "count" codeActions.Length >> Log.addContextDestructured "codeActions" codeActions ) + codeActions /// `ignore testCaseAsync` -/// +/// /// Like `testCaseAsync`, but test gets completely ignored. /// Unlike `ptestCaseAsync` (pending), this here doesn't even show up in Expecto summary. -/// +/// /// -> Used to mark issues & shortcomings in CodeFixes, but without any (immediate) intention to fix -/// (vs. `pending` -> marked for fixing) +/// (vs. `pending` -> marked for fixing) /// -> ~ uncommenting tests without actual uncommenting -let itestCaseAsync name test = () +let itestCaseAsync _name _test = () diff --git a/test/FsAutoComplete.Tests.Lsp/CodeLensTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeLensTests.fs index f5977e55b..655eff2aa 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeLensTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeLensTests.fs @@ -14,26 +14,21 @@ open Newtonsoft.Json.Linq open Helpers.Expecto.ShadowedTimeouts module private CodeLens = - let assertNoDiagnostics (ds: Diagnostic []) = + let assertNoDiagnostics (ds: Diagnostic[]) = match ds with | [||] -> Ok() | ds -> Error $"Expected no diagnostics, but got %A{ds}" let check server text checkLenses = asyncResult { - let textRange, text = - text - |> Text.trimTripleQuotation - |> Cursor.assertExtractRange + let textRange, text = text |> Text.trimTripleQuotation |> Cursor.assertExtractRange let! (doc, diags) = Server.createUntitledDocument text server do! assertNoDiagnostics diags let p: CodeLensParams = { TextDocument = doc.TextDocumentIdentifier } - let! lenses = - doc.Server.Server.TextDocumentCodeLens p - |> AsyncResult.mapError string + let! lenses = doc.Server.Server.TextDocumentCodeLens p |> AsyncResult.mapError string let! resolved = Option.toList lenses @@ -53,72 +48,80 @@ module private CodeLens = let tests state = serverTestList (nameof CodeLens) state defaultConfigDto None (fun server -> - [ testCaseAsync - "can show codelens for type annotation" <| - CodeLens.check server - """ + [ testCaseAsync "can show codelens for type annotation" + <| CodeLens.check server """ module X = $0let func x = x + 1$0 - """ (fun (doc, lenses) -> - Expect.hasLength lenses 2 "should have a type lens and a reference lens" - let typeLens = lenses[0] - Expect.equal typeLens.Command.Value.Title "int -> int" "first lens should be a type hint of int to int" - Expect.isNone typeLens.Command.Value.Arguments "No data required for type lenses" - Expect.equal typeLens.Command.Value.Command "" "No command for type lenses" - ) - - testCaseAsync - "can show codelens for 0 reference count" <| - CodeLens.check server - """ + """ (fun (_doc, lenses) -> + Expect.hasLength lenses 2 "should have a type lens and a reference lens" + let typeLens = lenses[0] + Expect.equal typeLens.Command.Value.Title "int -> int" "first lens should be a type hint of int to int" + Expect.isNone typeLens.Command.Value.Arguments "No data required for type lenses" + Expect.equal typeLens.Command.Value.Command "" "No command for type lenses") + + testCaseAsync "can show codelens for 0 reference count" + <| CodeLens.check server """ module X = $0let func x = x + 1$0 - """ (fun (doc, lenses) -> - Expect.hasLength lenses 2 "should have a type lens and a reference lens" - let referenceLens = lenses[1] - let emptyCommand = Some { Title = "0 References"; Arguments = None; Command = "" } - Expect.equal referenceLens.Command emptyCommand "There should be no command or args for zero references" - ) - testCaseAsync - "can show codelens for multi reference count" <| - CodeLens.check server - """ + """ (fun (_doc, lenses) -> + Expect.hasLength lenses 2 "should have a type lens and a reference lens" + let referenceLens = lenses[1] + + let emptyCommand = + Some + { Title = "0 References" + Arguments = None + Command = "" } + + Expect.equal referenceLens.Command emptyCommand "There should be no command or args for zero references") + testCaseAsync "can show codelens for multi reference count" + <| CodeLens.check server """ module X = $0let func x = x + 1$0 let doThing () = func 1 """ (fun (doc, lenses) -> - Expect.hasLength lenses 2 "should have a type lens and a reference lens" - let referenceLens = lenses[1] - Expect.isSome referenceLens.Command "There should be a command for multiple references" - let referenceCommand = referenceLens.Command.Value - Expect.equal referenceCommand.Title "1 References" "There should be a title for multiple references" - Expect.equal referenceCommand.Command "fsharp.showReferences" "There should be a command for multiple references" - Expect.isSome referenceCommand.Arguments "There should be arguments for multiple references" - let args = referenceCommand.Arguments.Value - Expect.equal args.Length 3 "There should be 2 args" - let filePath, triggerPos, referenceRanges = - args[0].Value(), - (args[1] :?> JObject).ToObject(), - (args[2] :?> JArray) |> Seq.map (fun t -> (t:?>JObject).ToObject()) |> Array.ofSeq - Expect.equal filePath doc.Uri "File path should be the doc we're checking" - Expect.equal triggerPos { Line = 1; Character = 6 } "Position should be 1:6" - Expect.hasLength referenceRanges 1 "There should be 1 reference range for the `func` function" - Expect.equal referenceRanges[0] { Uri = doc.Uri; Range = { Start = { Line = 3; Character = 19 }; End = { Line = 3; Character = 23 } } } "Reference range should be 0:0" - ) - testCaseAsync "can show reference counts for 1-character identifier" <| - CodeLens.check server - """ - $0let f () = ""$0 - """ (fun (doc, lenses) -> - Expect.hasLength lenses 2 "should have a type lens and a reference lens" - let referenceLens = lenses[1] - Expect.isSome referenceLens.Command "There should be a command for multiple references" - let referenceCommand = referenceLens.Command.Value - Expect.equal referenceCommand.Title "0 References" "There should be a title for multiple references" - Expect.equal referenceCommand.Command "" "There should be no command for multiple references" - Expect.isNone referenceCommand.Arguments "There should be arguments for multiple references" - ) - ] - ) + Expect.hasLength lenses 2 "should have a type lens and a reference lens" + let referenceLens = lenses[1] + Expect.isSome referenceLens.Command "There should be a command for multiple references" + let referenceCommand = referenceLens.Command.Value + Expect.equal referenceCommand.Title "1 References" "There should be a title for multiple references" + + Expect.equal + referenceCommand.Command + "fsharp.showReferences" + "There should be a command for multiple references" + + Expect.isSome referenceCommand.Arguments "There should be arguments for multiple references" + let args = referenceCommand.Arguments.Value + Expect.equal args.Length 3 "There should be 2 args" + let filePath, triggerPos, referenceRanges = + args[0].Value(), + (args[1] :?> JObject).ToObject(), + (args[2] :?> JArray) + |> Seq.map (fun t -> (t :?> JObject).ToObject()) + |> Array.ofSeq + + Expect.equal filePath doc.Uri "File path should be the doc we're checking" + Expect.equal triggerPos { Line = 1; Character = 6 } "Position should be 1:6" + Expect.hasLength referenceRanges 1 "There should be 1 reference range for the `func` function" + + Expect.equal + referenceRanges[0] + { Uri = doc.Uri + Range = + { Start = { Line = 3; Character = 19 } + End = { Line = 3; Character = 23 } } } + "Reference range should be 0:0") + testCaseAsync "can show reference counts for 1-character identifier" + <| CodeLens.check server """ + $0let f () = ""$0 + """ (fun (_doc, lenses) -> + Expect.hasLength lenses 2 "should have a type lens and a reference lens" + let referenceLens = lenses[1] + Expect.isSome referenceLens.Command "There should be a command for multiple references" + let referenceCommand = referenceLens.Command.Value + Expect.equal referenceCommand.Title "0 References" "There should be a title for multiple references" + Expect.equal referenceCommand.Command "" "There should be no command for multiple references" + Expect.isNone referenceCommand.Arguments "There should be arguments for multiple references") ]) diff --git a/test/FsAutoComplete.Tests.Lsp/CompletionTests.fs b/test/FsAutoComplete.Tests.Lsp/CompletionTests.fs index a4928143d..b519a34ac 100644 --- a/test/FsAutoComplete.Tests.Lsp/CompletionTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CompletionTests.fs @@ -1,13 +1,13 @@ module FsAutoComplete.Tests.Completion -open Expecto +open System open System.IO +open Expecto open Helpers open Ionide.LanguageServerProtocol.Types open FsAutoComplete.Utils open FsAutoComplete.Lsp open FsToolkit.ErrorHandling -open Utils.Server open Helpers.Expecto.ShadowedTimeouts let tests state = @@ -20,7 +20,7 @@ let tests state = let tdop: DidOpenTextDocumentParams = { TextDocument = loadDocument path } do! server.TextDocumentDidOpen tdop - let! diagnostics = + let! _diagnostics = waitForParseResultsForFile "Script.fsx" events |> AsyncResult.bimap (fun _ -> failtest "Should have had errors") (fun e -> e) @@ -759,7 +759,7 @@ let autoOpenTests state = { Line = pos.Line Character = indentation } - let getQuickFix (server: IFSharpLspServer, path: string) (word: string, ns: string) (cursor: Position) = + let getQuickFix (server: IFSharpLspServer, path: string) (word: string, _ns: string) (cursor: Position) = async { let p = { CodeActionParams.TextDocument = { Uri = Path.FilePathToUri path } @@ -781,7 +781,7 @@ let autoOpenTests state = let (|ContainsOpenAction|_|) (codeActions: CodeAction[]) = codeActions - |> Array.tryFind (fun ca -> ca.Kind = Some "quickfix" && ca.Title.StartsWith "open ") + |> Array.tryFind (fun ca -> ca.Kind = Some "quickfix" && ca.Title.StartsWith("open ", StringComparison.Ordinal)) match! server.TextDocumentCodeAction p with | Error e -> return failtestf "Quick fix Request failed: %A" e @@ -1062,22 +1062,31 @@ let fullNameExternalAutocompleteTest state = let count = items - |> Array.distinctBy (function Ok x -> x.Detail | Error _ -> None) + |> Array.distinctBy (function + | Ok x -> x.Detail + | Error _ -> None) |> Array.length Expect.equal count items.Length "These completions doesn't have different description" } |> AsyncResult.bimap id (fun e -> failwithf "%O" e)) - makeAutocompleteTest - serverConfig - "Check Autocomplete for System.Text.RegularExpressions.Regex" - (4, 5) - (fun res -> - let n = res.Items |> Array.tryFind (fun i -> i.Label = "Regex (System.Text.RegularExpressions)") - Expect.isSome n "Completion doesn't exist" - Expect.equal n.Value.InsertText (Some "System.Text.RegularExpressions.Regex") "Autocomplete for Regex is not System.Text.RegularExpressions.Regex" - Expect.equal n.Value.FilterText (Some "RegexSystem.Text.RegularExpressions.Regex") "Autocomplete for Regex is not System.Text.RegularExpressions.Regex") + makeAutocompleteTest serverConfig "Check Autocomplete for System.Text.RegularExpressions.Regex" (4, 5) (fun res -> + let n = + res.Items + |> Array.tryFind (fun i -> i.Label = "Regex (System.Text.RegularExpressions)") + + Expect.isSome n "Completion doesn't exist" + + Expect.equal + n.Value.InsertText + (Some "System.Text.RegularExpressions.Regex") + "Autocomplete for Regex is not System.Text.RegularExpressions.Regex" + + Expect.equal + n.Value.FilterText + (Some "RegexSystem.Text.RegularExpressions.Regex") + "Autocomplete for Regex is not System.Text.RegularExpressions.Regex") makeAutocompleteTest serverConfig "Autocomplete for Result is just Result" (5, 6) (fun res -> let n = res.Items |> Array.tryFind (fun i -> i.Label = "Result") diff --git a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs index ed6a8b761..fbc24265a 100644 --- a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs @@ -32,7 +32,7 @@ let initTests createServer = let tempDir = Path.Combine(Path.GetTempPath(), "FsAutoComplete.Tests", Guid.NewGuid().ToString()) - let (server: IFSharpLspServer, event) = createServer () + let (server: IFSharpLspServer, _event) = createServer () let p: InitializeParams = { ProcessId = Some 1 @@ -59,9 +59,11 @@ let initTests createServer = Expect.equal res.Capabilities.CodeActionProvider - (Some (U2.Second - { CodeActionOptions.ResolveProvider = None - CodeActionOptions.CodeActionKinds = None })) + (Some( + U2.Second + { CodeActionOptions.ResolveProvider = None + CodeActionOptions.CodeActionKinds = None } + )) "Code Action Provider" Expect.equal @@ -75,15 +77,18 @@ let initTests createServer = Expect.equal res.Capabilities.DocumentLinkProvider None "Document Link Provider" Expect.equal res.Capabilities.DocumentOnTypeFormattingProvider None "Document OnType Formatting Provider" Expect.equal res.Capabilities.DocumentRangeFormattingProvider (Some true) "Document Range Formatting Provider" - Expect.equal res.Capabilities.DocumentSymbolProvider (Some (U2.Second { Label = Some "F#" })) "Document Symbol Provider" + + Expect.equal + res.Capabilities.DocumentSymbolProvider + (Some(U2.Second { Label = Some "F#" })) + "Document Symbol Provider" + Expect.equal res.Capabilities.ExecuteCommandProvider None "Execute Command Provider" Expect.equal res.Capabilities.Experimental None "Experimental" Expect.equal res.Capabilities.HoverProvider (Some true) "Hover Provider" Expect.equal res.Capabilities.ImplementationProvider (Some true) "Implementation Provider" Expect.equal res.Capabilities.ReferencesProvider (Some true) "References Provider" - Expect.equal res.Capabilities.RenameProvider (Some(U2.Second { - PrepareProvider = Some true - })) "Rename Provider" + Expect.equal res.Capabilities.RenameProvider (Some(U2.Second { PrepareProvider = Some true })) "Rename Provider" Expect.equal res.Capabilities.SignatureHelpProvider @@ -100,9 +105,14 @@ let initTests createServer = Expect.equal res.Capabilities.TextDocumentSync (Some td) "Text Document Provider" Expect.equal res.Capabilities.TypeDefinitionProvider (Some true) "Type Definition Provider" - Expect.equal res.Capabilities.WorkspaceSymbolProvider (Some (U2.Second { ResolveProvider = Some true })) "Workspace Symbol Provider" + + Expect.equal + res.Capabilities.WorkspaceSymbolProvider + (Some(U2.Second { ResolveProvider = Some true })) + "Workspace Symbol Provider" + Expect.equal res.Capabilities.FoldingRangeProvider (Some true) "Folding Range Provider active" - | Result.Error e -> failtest "Initialization failed" + | Result.Error _e -> failtest "Initialization failed" }) ///Tests for getting document symbols @@ -110,7 +120,7 @@ let documentSymbolTest state = let server = async { let path = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "DocumentSymbolTest") - let! (server, event) = serverInitialize path defaultConfigDto state + let! (server, _event) = serverInitialize path defaultConfigDto state let path = Path.Combine(path, "Script.fsx") let tdop: DidOpenTextDocumentParams = { TextDocument = loadDocument path } do! server.TextDocumentDidOpen tdop @@ -137,7 +147,7 @@ let documentSymbolTest state = res (fun n -> n.Name = "MyDateTime" && n.Kind = SymbolKind.Class) "Document symbol contains given symbol" - | Result.Ok(Some(U2.Second res)) -> raise (NotImplementedException("DocumentSymbol isn't used in FSAC yet")) + | Result.Ok(Some(U2.Second _res)) -> raise (NotImplementedException("DocumentSymbol isn't used in FSAC yet")) }) ] let foldingTests state = @@ -177,29 +187,32 @@ let tooltipTests state = let (|Signature|_|) (hover: Hover) = match hover with | { Contents = MarkedStrings [| MarkedString.WithLanguage { Language = "fsharp"; Value = tooltip } - MarkedString.String docComment - MarkedString.String fullname - MarkedString.String assembly |] } -> Some tooltip + MarkedString.String _docComment + MarkedString.String _fullname + MarkedString.String _assembly |] } -> Some tooltip | { Contents = MarkedStrings [| MarkedString.WithLanguage { Language = "fsharp"; Value = tooltip } - MarkedString.String docComment - MarkedString.String showDocumentationLink - MarkedString.String fullname - MarkedString.String assembly |] } -> Some tooltip + MarkedString.String _docComment + MarkedString.String _showDocumentationLink + MarkedString.String _fullname + MarkedString.String _assembly |] } -> Some tooltip | _ -> None let (|Description|_|) (hover: Hover) = match hover with - | { Contents = MarkedStrings [| MarkedString.WithLanguage { Language = "fsharp"; Value = tooltip } + | { Contents = MarkedStrings [| MarkedString.WithLanguage { Language = "fsharp" + Value = _tooltip } MarkedString.String description |] } -> Some description - | { Contents = MarkedStrings [| MarkedString.WithLanguage { Language = "fsharp"; Value = tooltip } + | { Contents = MarkedStrings [| MarkedString.WithLanguage { Language = "fsharp" + Value = _tooltip } MarkedString.String description - MarkedString.String fullname - MarkedString.String assembly |] } -> Some description - | { Contents = MarkedStrings [| MarkedString.WithLanguage { Language = "fsharp"; Value = tooltip } + MarkedString.String _fullname + MarkedString.String _assembly |] } -> Some description + | { Contents = MarkedStrings [| MarkedString.WithLanguage { Language = "fsharp" + Value = _tooltip } MarkedString.String description - MarkedString.String showDocumentationLink - MarkedString.String fullname - MarkedString.String assembly |] } -> Some description + MarkedString.String _showDocumentationLink + MarkedString.String _fullname + MarkedString.String _assembly |] } -> Some description | _ -> None let server = @@ -259,12 +272,6 @@ let tooltipTests state = let verifyDescription line character expectedDescription = verifyDescriptionImpl testCaseAsync line character expectedDescription - let pverifyDescription reason line character expectedDescription = - verifyDescriptionImpl ptestCaseAsync line character expectedDescription - - let fverifyDescription line character expectedDescription = - verifyDescriptionImpl ftestCaseAsync line character expectedDescription - testSequenced <| testList "tooltip evaluation" @@ -372,7 +379,28 @@ let tooltipTests state = " body : (MailboxProcessor -> Async) *" " cancellationToken: option" " -> MailboxProcessor" ]) - verifySignature 54 9 "Case2 of string * newlineBefore: bool * newlineAfter: bool" ] ] + verifySignature 54 9 "Case2 of string * newlineBefore: bool * newlineAfter: bool" + verifySignature + 60 + 7 + (concatLines + [ "active pattern Value: " + " input: Expr" + " -> option" ]) + verifySignature + 65 + 7 + (concatLines + [ "active pattern DefaultValue: " + " input: Expr" + " -> option" ]) + verifySignature + 70 + 7 + (concatLines + [ "active pattern ValueWithName: " + " input: Expr" + " -> option" ]) ] ] let closeTests state = // Note: clear diagnostics also implies clear caches (-> remove file & project options from State). diff --git a/test/FsAutoComplete.Tests.Lsp/ExtensionsTests.fs b/test/FsAutoComplete.Tests.Lsp/ExtensionsTests.fs index c0b1a6837..210096409 100644 --- a/test/FsAutoComplete.Tests.Lsp/ExtensionsTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/ExtensionsTests.fs @@ -77,10 +77,7 @@ let uriTests = testList "Uri tests" - [ testList - "roundtrip tests" - (samples - |> List.map (fun (uriForm, filePath) -> verifyUri uriForm filePath)) + [ testList "roundtrip tests" (samples |> List.map (fun (uriForm, filePath) -> verifyUri uriForm filePath)) testList "fileName to uri tests" (samples @@ -91,7 +88,14 @@ let linterTests state = let server = async { let path = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "LinterTest") - let! (server, events) = serverInitialize path { defaultConfigDto with Linter = Some true } state + + let! (server, events) = + serverInitialize + path + { defaultConfigDto with + Linter = Some true } + state + let path = Path.Combine(path, "Script.fsx") let tdop: DidOpenTextDocumentParams = { TextDocument = loadDocument path } do! server.TextDocumentDidOpen tdop @@ -274,14 +278,23 @@ let formattingTests state = do! server.TextDocumentDidOpen { TextDocument = loadDocument sourceFile } match! waitForParseResultsForFile (Path.GetFileName sourceFile) events with - | Ok () -> + | Ok() -> match! server.TextDocumentFormatting { TextDocument = { Uri = Path.FilePathToUri sourceFile } - Options = {TabSize = 4; InsertSpaces = true; TrimTrailingWhitespace = None; InsertFinalNewline = None; TrimFinalNewlines = None; AdditionalData = System.Collections.Generic.Dictionary<_,_>() } } - with - | Ok (Some [| edit |]) -> - let normalized = { edit with NewText = normalizeLineEndings edit.NewText } + Options = + { TabSize = 4 + InsertSpaces = true + TrimTrailingWhitespace = None + InsertFinalNewline = None + TrimFinalNewlines = None + AdditionalData = System.Collections.Generic.Dictionary<_, _>() } } + with + | Ok(Some [| edit |]) -> + let normalized = + { edit with + NewText = normalizeLineEndings edit.NewText } + Expect.equal normalized expectedTextEdit "should replace the entire file range with the expected content" | Ok other -> failwithf "Invalid formatting result: %A" other | Result.Error e -> failwithf "Error while formatting %s: %A" sourceFile e @@ -302,13 +315,7 @@ let analyzerTests state = // because the analyzer is a project this project has a reference, the analyzer can be // found in alongside this project, so we can use the directory this project is in let analyzerPath = - System.IO.Path.GetDirectoryName( - System - .Reflection - .Assembly - .GetExecutingAssembly() - .Location - ) + System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) let analyzerEnabledConfig = { defaultConfigDto with @@ -331,7 +338,7 @@ let analyzerTests state = [ testCaseAsync "can run analyzer on file" (async { - let! (server, events, rootPath, testFilePath) = server + let! _server, events, _rootPath, testFilePath = server let! diagnostics = analyzerDiagnostics (System.IO.Path.GetFileName testFilePath) events @@ -363,7 +370,7 @@ let signatureTests state = do! server.TextDocumentDidOpen { TextDocument = loadDocument scriptPath } match! waitForParseResultsForFile "Script.fsx" events with - | Ok () -> () // all good, no parsing/checking errors + | Ok() -> () // all good, no parsing/checking errors | Core.Result.Error errors -> failtestf "Errors while parsing script %s: %A" scriptPath errors return server, scriptPath @@ -380,7 +387,7 @@ let signatureTests state = Position = { Line = line; Character = character } } match! server.FSharpSignature pos with - | Ok (Some { Content = content }) -> + | Ok(Some { Content = content }) -> let r = JsonSerializer.readJson> (content) Expect.equal r.Kind "typesig" "Should have a kind of 'typesig'" diff --git a/test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs b/test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs index a242b5f61..98b12a822 100644 --- a/test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs @@ -1,7 +1,8 @@ module FsAutoComplete.Tests.FindReferences -open Expecto +open System open System.IO +open Expecto open FsAutoComplete open Helpers open Ionide.LanguageServerProtocol.Types @@ -17,10 +18,12 @@ open FSharp.Compiler.CodeAnalysis open Helpers.Expecto.ShadowedTimeouts let private scriptTests state = - testList "script" + testList + "script" [ let server = async { - let path = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "FindReferences", "Script") + let path = + Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "FindReferences", "Script") let! (server, event) = serverInitialize path defaultConfigDto state do! waitForWorkspaceFinishedParsing event @@ -48,7 +51,7 @@ let private scriptTests state = match response with | Ok None -> failtestf "Should have gotten some references for this identifier" | Error e -> failtestf "Errored while getting references for identifier: %A" e - | Ok (Some references) -> + | Ok(Some references) -> Expect.hasLength references 2 "Should have a reference for the definition and usage" let reference = references.[0] Expect.stringEnds reference.Uri (Path.GetFileName scriptPath) "should point to the same script" @@ -71,48 +74,47 @@ let private extractRanges (sourceWithCursors: string) = let (source, cursors) = sourceWithCursors |> Text.trimTripleQuotation - |> Cursors.extractWith [| Cursor.cursor; Cursor.usageStart; Cursor.usageEnd; Cursor.defStart; Cursor.defEnd |] + |> Cursors.extractWith + [| Cursor.cursor + Cursor.usageStart + Cursor.usageEnd + Cursor.defStart + Cursor.defEnd |] let cursor, cursors = - let cs, cursors = - cursors - |> List.partition (fst >> (=) Cursor.cursor) + let cs, cursors = cursors |> List.partition (fst >> (=) Cursor.cursor) + if cs |> List.isEmpty then (None, cursors) else Expect.hasLength cs 1 "There should be either 0 or 1 cursor $0" - (Some (snd cs[0]), cursors) + (Some(snd cs[0]), cursors) - let mkRange start fin = { - Start = start - End = fin - } + let mkRange start fin = { Start = start; End = fin } - let rec collectRanges (cursors: (string*Position) list) ((decls, usages) as ranges) = + let rec collectRanges (cursors: (string * Position) list) ((decls, usages) as ranges) = match cursors with | [] -> ranges - | [(c,p)] -> failwith $"Lonely last cursor {c} at {p}" - | (c1,p1)::(c2,p2)::cursors when c1 = Cursor.usageStart && c2 = Cursor.usageEnd -> - let range = mkRange p1 p2 - let ranges = (range :: decls, usages) - collectRanges cursors ranges - | (c1,p1)::(c2,p2) :: cursors when c1 = Cursor.defStart && c2 = Cursor.defEnd -> - let range = mkRange p1 p2 - let ranges = (decls, range :: usages) - collectRanges cursors ranges - | (c1,p1)::(c2,p2):: _ -> - failwith $"Cursor pair {c1} & {c2} do not match (at {p1} & {p2})" - let (decls, usages) = collectRanges cursors ([],[]) - source, {| - Cursor = cursor - Declarations = decls |> List.rev |> List.toArray - Usages = usages |> List.rev |> List.toArray - |} -let private mkLocation doc range = - { - Uri = doc.Uri - Range = range - } + | [ (c, p) ] -> failwith $"Lonely last cursor {c} at {p}" + | (c1, p1) :: (c2, p2) :: cursors when c1 = Cursor.usageStart && c2 = Cursor.usageEnd -> + let range = mkRange p1 p2 + let ranges = (range :: decls, usages) + collectRanges cursors ranges + | (c1, p1) :: (c2, p2) :: cursors when c1 = Cursor.defStart && c2 = Cursor.defEnd -> + let range = mkRange p1 p2 + let ranges = (decls, range :: usages) + collectRanges cursors ranges + | (c1, p1) :: (c2, p2) :: _ -> failwith $"Cursor pair {c1} & {c2} do not match (at {p1} & {p2})" + + let (decls, usages) = collectRanges cursors ([], []) + + source, + {| Cursor = cursor + Declarations = decls |> List.rev |> List.toArray + Usages = usages |> List.rev |> List.toArray |} + +let private mkLocation doc range = { Uri = doc.Uri; Range = range } + /// mark locations in text /// -> differences gets highlighted in source instead of Location array /// @@ -122,14 +124,16 @@ let private markRanges (source: string) (locs: Location[]) = locs |> Array.map (fun l -> l.Range) |> Array.sortByDescending (fun r -> (r.Start.Line, r.Start.Character)) + ranges - |> Array.fold (fun source range -> + |> Array.fold + (fun source range -> source |> Text.insert range.End "〉" |> Flip.Expect.wantOk "Should be valid insert position" |> Text.insert range.Start "〈" - |> Flip.Expect.wantOk "Should be valid insert position" - ) source + |> Flip.Expect.wantOk "Should be valid insert position") + source module Expect = /// `exact`: @@ -151,29 +155,35 @@ module Expect = let inspect () = // Note: simplification: only find 1st doc that differs let actualByUri, expectedByUri = - ( - actual |> Array.groupBy (fun l -> l.Uri) |> Array.sortBy fst, - expected |> Array.groupBy (fun l -> l.Uri) |> Array.sortBy fst - ) + (actual |> Array.groupBy (fun l -> l.Uri) |> Array.sortBy fst, + expected |> Array.groupBy (fun l -> l.Uri) |> Array.sortBy fst) // cannot directly use zip: might be unequal number of docs - Expect.sequenceEqual (actualByUri |> Array.map fst) (expectedByUri |> Array.map fst) "Should find references in correct docs" + Expect.sequenceEqual + (actualByUri |> Array.map fst) + (expectedByUri |> Array.map fst) + "Should find references in correct docs" // from here on: actualByUri & expectedByUri have same length and same docs in same order (because sorted) for ((uri, actual), (_, expected)) in Array.zip actualByUri expectedByUri do let source = getSource uri if exact then - Expect.equal (markRanges source actual) (markRanges source expected) $"Should find correct & exact references in doc %s{uri}" + Expect.equal + (markRanges source actual) + (markRanges source expected) + $"Should find correct & exact references in doc %s{uri}" else let actual = actual |> Array.sortBy (fun l -> l.Range.Start) let expected = expected |> Array.sortBy (fun l -> l.Range.Start) // actual & expected might have different length // again: find only first difference - let exactDisclaimer = "\nNote: Ranges in actual might be longer than in expected. That's ok because `exact=false`\n" + let exactDisclaimer = + "\nNote: Ranges in actual might be longer than in expected. That's ok because `exact=false`\n" if actual.Length <> expected.Length then - let msg = $"Found %i{actual.Length} references in doc %s{uri}, but expected %i{expected.Length} references.%s{exactDisclaimer}" + let msg = + $"Found %i{actual.Length} references in doc %s{uri}, but expected %i{expected.Length} references.%s{exactDisclaimer}" // this fails -> used for pretty printing of diff Expect.equal (markRanges source actual) (markRanges source expected) msg @@ -181,19 +191,17 @@ module Expect = // expected must fit into actual let inside = aLoc.Range |> Range.containsStrictly eLoc.Range.Start - && - aLoc.Range |> Range.containsStrictly eLoc.Range.End + && aLoc.Range |> Range.containsStrictly eLoc.Range.End if not inside then let msg = $"%i{i}. reference inside %s{uri} has incorrect range.%s{exactDisclaimer}" - Expect.equal (markRanges source [|aLoc|]) (markRanges source [|eLoc|]) msg + Expect.equal (markRanges source [| aLoc |]) (markRanges source [| eLoc |]) msg if exact then try Expect.sequenceEqual actual expected "Should find all references with correct range" - with - | :? AssertException -> + with :? AssertException -> // pretty printing: Source with marked locations instead of lists with locations inspect () else @@ -202,6 +210,7 @@ module Expect = let private solutionTests state = let marker = "//>" + /// Format of Locations in file `path`: /// In line after range: /// * Mark start of line with `//>` @@ -227,33 +236,31 @@ let private solutionTests state = let readReferences path = let lines = File.ReadAllLines path let refs = Dictionary>() - for i in 0..(lines.Length-1) do + + for i in 0 .. (lines.Length - 1) do let line = lines[i].TrimStart() - if line.StartsWith marker then + + if line.StartsWith(marker, StringComparison.Ordinal) then let l = line.Substring(marker.Length).Trim() - let splits = l.Split([|' '|], 2) + let splits = l.Split([| ' ' |], 2) let mark = splits[0] - let ty = mark[0] + let _ty = mark[0] + let range = - let col = line.IndexOf mark + let col = line.IndexOf(mark, StringComparison.Ordinal) let length = mark.Length - let line = i - 1 // marker is line AFTER actual range - { - Start = { Line = line; Character = col } - End = { Line = line; Character = col + length } - } - let loc = { - Uri = - path - |> normalizePath - |> Path.LocalPathToUri - Range = range - } - let name = - if splits.Length > 1 then - splits[1] - else - "" + let line = i - 1 // marker is line AFTER actual range + + { Start = { Line = line; Character = col } + End = + { Line = line + Character = col + length } } + + let loc = + { Uri = path |> normalizePath |> Path.LocalPathToUri + Range = range } + + let name = if splits.Length > 1 then splits[1] else "" if not (refs.ContainsKey name) then refs[name] <- List<_>() @@ -261,97 +268,113 @@ let private solutionTests state = let existing = refs[name] // Note: we're currently dismissing type (declaration, usage) existing.Add loc |> ignore + refs let readAllReferences dir = // `.fs` & `.fsx` let files = Directory.GetFiles(dir, "*.fs*", SearchOption.AllDirectories) + files |> Seq.map readReferences - |> Seq.map (fun dict -> - dict - |> Seq.map (fun kvp -> kvp.Key, kvp.Value) - ) + |> Seq.map (fun dict -> dict |> Seq.map (fun kvp -> kvp.Key, kvp.Value)) |> Seq.collect id |> Seq.groupBy fst |> Seq.map (fun (name, locs) -> (name, locs |> Seq.map snd |> Seq.collect id |> Seq.toArray)) - |> Seq.map (fun (name, locs) -> {| Name=name; Locations=locs |}) + |> Seq.map (fun (name, locs) -> {| Name = name; Locations = locs |}) |> Seq.toArray - let path = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "FindReferences", "Solution") - serverTestList "solution" state defaultConfigDto (Some path) (fun server -> [ - // extra function instead of (just) testCase in front: to be able to run single test case - let mutable scriptFilesLoaded = false - let assertScriptFilesLoaded = async { - if not scriptFilesLoaded then - let files = Directory.GetFiles(path, "*.fsx", SearchOption.AllDirectories) - for file in files do - let relativePath = Path.GetRelativePath(path, file) - let! (doc, _) = server |> Server.openDocument relativePath - // keep script files open: - // Unlike `FsharpLspServer`, AdaptiveLspServer doesn't keep closed scripts in cache - // -> cannot find references inside closed script files - // see https://github.com/fsharp/FsAutoComplete/pull/1037#issuecomment-1440016138 - // do! doc |> Document.close - () - scriptFilesLoaded <- true - } - testCaseAsync "open script files" (async { - // script files aren't loaded in background - // -> cannot find references in unopened script files + let path = + Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "FindReferences", "Solution") - // -> load script files (don't need to be kept open -- just loaded once -> in FSAC cache) - do! assertScriptFilesLoaded + serverTestList "solution" state defaultConfigDto (Some path) (fun server -> + [ + // extra function instead of (just) testCase in front: to be able to run single test case + let mutable scriptFilesLoaded = false - //Enhancement: implement auto-load (like for projects)? - }) + let assertScriptFilesLoaded = + async { + if not scriptFilesLoaded then + let files = Directory.GetFiles(path, "*.fsx", SearchOption.AllDirectories) + + for file in files do + let relativePath = Path.GetRelativePath(path, file) + let! (_doc, _) = server |> Server.openDocument relativePath + // keep script files open: + // Unlike `FsharpLspServer`, AdaptiveLspServer doesn't keep closed scripts in cache + // -> cannot find references inside closed script files + // see https://github.com/fsharp/FsAutoComplete/pull/1037#issuecomment-1440016138 + // do! doc |> Document.close + () + + scriptFilesLoaded <- true + } + + testCaseAsync + "open script files" + (async { + // script files aren't loaded in background + // -> cannot find references in unopened script files - let mainDoc = Path.Combine("B", "WorkingModule.fs") - documentTestList "inside B/WorkingModule.fs" server (Server.openDocument mainDoc) (fun doc -> [ - let refs = readAllReferences path - for r in refs do - testCaseAsync r.Name (async { + // -> load script files (don't need to be kept open -- just loaded once -> in FSAC cache) do! assertScriptFilesLoaded - let! (doc, _) = doc - let cursor = - let cursor = - r.Locations - |> Seq.filter (fun l -> l.Uri = doc.Uri) - |> Seq.minBy (fun l -> l.Range.Start) - cursor.Range.Start + //Enhancement: implement auto-load (like for projects)? + }) - let request: ReferenceParams = - { TextDocument = doc.TextDocumentIdentifier - Position = cursor - Context = { IncludeDeclaration = true } } - let! refs = doc.Server.Server.TextDocumentReferences request - let refs = - refs - |> Flip.Expect.wantOk "Should not fail" - |> Flip.Expect.wantSome "Should return references" + let mainDoc = Path.Combine("B", "WorkingModule.fs") - let expected = r.Locations + documentTestList "inside B/WorkingModule.fs" server (Server.openDocument mainDoc) (fun doc -> + [ let refs = readAllReferences path - let getSource uri = - let path = Path.FileUriToLocalPath uri - File.ReadAllText path + for r in refs do + testCaseAsync + r.Name + (async { + do! assertScriptFilesLoaded - Expect.locationsEqual getSource false refs expected - }) - ]) - ]) + let! (doc, _) = doc + + let cursor = + let cursor = + r.Locations + |> Seq.filter (fun l -> l.Uri = doc.Uri) + |> Seq.minBy (fun l -> l.Range.Start) + + cursor.Range.Start + + let request: ReferenceParams = + { TextDocument = doc.TextDocumentIdentifier + Position = cursor + Context = { IncludeDeclaration = true } } + + let! refs = doc.Server.Server.TextDocumentReferences request + + let refs = + refs + |> Flip.Expect.wantOk "Should not fail" + |> Flip.Expect.wantSome "Should return references" + + let expected = r.Locations + + let getSource uri = + let path = Path.FileUriToLocalPath uri + File.ReadAllText path + + Expect.locationsEqual getSource false refs expected + }) ]) ]) /// multiple untitled files (-> all docs are unrelated) /// -> Tests for external symbols (-> over all docs) & symbol just in current doc (-> no matches in other unrelated docs) let private untitledTests state = - serverTestList "untitled" state defaultConfigDto None (fun server -> [ - testCaseAsync "can find external `Delay` in all open untitled docs" (async { - // Note: Cursor MUST be in first source - let sources = - [| - """ + serverTestList "untitled" state defaultConfigDto None (fun server -> + [ testCaseAsync + "can find external `Delay` in all open untitled docs" + (async { + // Note: Cursor MUST be in first source + let sources = + [| """ open System open System.Threading.Tasks let _ = task { @@ -366,7 +389,7 @@ let private untitledTests state = .$$ (TimeSpan.MaxValue) } """ - """ + """ open System open System.Threading.Tasks let _ = task { @@ -378,68 +401,62 @@ let private untitledTests state = do! Task.$$ (TimeSpan.MaxValue) } """ - // No Task.Delay - """ + // No Task.Delay + """ open System printfn "do stuff" - """ - |] - |> Array.map (extractRanges) - - let! docs = - sources - |> Seq.map fst - |> Seq.map (fun source -> async { - let! (doc, diags) = server |> Server.createUntitledDocument source - Expect.hasLength diags 0 $"There should be no diags in doc {doc.Uri}" - return doc - }) - |> Async.Sequential - - let (cursorDoc, cursor) = - let cursors = - Array.zip docs sources - |> Array.choose (fun (doc, (_, cursors)) -> - cursors.Cursor - |> Option.map (fun cursor -> (doc, cursor)) - ) - Expect.hasLength cursors 1 "There should be exactly one cursor" - cursors[0] - let request: ReferenceParams = - { TextDocument = cursorDoc.TextDocumentIdentifier - Position = cursor - Context = { IncludeDeclaration = true } } - let! refs = cursorDoc.Server.Server.TextDocumentReferences request - let refs = - refs - |> Flip.Expect.wantOk "Should not fail" - |> Flip.Expect.wantSome "Should return references" + """ |] + |> Array.map (extractRanges) + + let! docs = + sources + |> Seq.map fst + |> Seq.map (fun source -> + async { + let! (doc, diags) = server |> Server.createUntitledDocument source + Expect.hasLength diags 0 $"There should be no diags in doc {doc.Uri}" + return doc + }) + |> Async.Sequential + + let (cursorDoc, cursor) = + let cursors = + Array.zip docs sources + |> Array.choose (fun (doc, (_, cursors)) -> cursors.Cursor |> Option.map (fun cursor -> (doc, cursor))) + + Expect.hasLength cursors 1 "There should be exactly one cursor" + cursors[0] - let expected = - Array.zip docs sources - |> Array.collect (fun (doc, (_, cursors)) -> - Array.append cursors.Declarations cursors.Usages - |> Array.map (mkLocation doc) - ) + let request: ReferenceParams = + { TextDocument = cursorDoc.TextDocumentIdentifier + Position = cursor + Context = { IncludeDeclaration = true } } - let getSource uri = - let i = docs |> Array.findIndex (fun doc -> doc.Uri = uri) - fst sources[i] + let! refs = cursorDoc.Server.Server.TextDocumentReferences request - Expect.locationsEqual getSource true refs expected + let refs = + refs + |> Flip.Expect.wantOk "Should not fail" + |> Flip.Expect.wantSome "Should return references" - }) - ]) + let expected = + Array.zip docs sources + |> Array.collect (fun (doc, (_, cursors)) -> + Array.append cursors.Declarations cursors.Usages |> Array.map (mkLocation doc)) + + let getSource uri = + let i = docs |> Array.findIndex (fun doc -> doc.Uri = uri) + fst sources[i] + + Expect.locationsEqual getSource true refs expected + + }) ]) /// Tests to check references span the correct range. For example: `Delay`, not `Task.Delay` let private rangeTests state = - let checkRanges - server - sourceWithCursors - = async { - let (source, cursors) = - sourceWithCursors - |> extractRanges + let checkRanges server sourceWithCursors = + async { + let (source, cursors) = sourceWithCursors |> extractRanges let! (doc, diags) = server |> Server.createUntitledDocument source use doc = doc Expect.hasLength diags 0 "There should be no diags" @@ -448,7 +465,9 @@ let private rangeTests state = { TextDocument = doc.TextDocumentIdentifier Position = cursors.Cursor.Value Context = { IncludeDeclaration = true } } + let! refs = doc.Server.Server.TextDocumentReferences request + let refs = refs |> Flip.Expect.wantOk "Should not fail" @@ -466,9 +485,11 @@ let private rangeTests state = if refs <> expected then Expect.equal (markRanges source refs) (markRanges source expected) "Should find correct references" } - serverTestList "range" state defaultConfigDto None (fun server -> [ - testCaseAsync "can get range of variable" <| - checkRanges server + + serverTestList "range" state defaultConfigDto None (fun server -> + [ testCaseAsync "can get range of variable" + <| checkRanges + server """ module MyModule = let $DD$ = 42 @@ -479,8 +500,9 @@ let private rangeTests state = let _ = MyModule.$$ + 42 let _ = MyModule.$<``value``>$ + 42 """ - testCaseAsync "can get range of external function" <| - checkRanges server + testCaseAsync "can get range of external function" + <| checkRanges + server """ open System open System.Threading.Tasks @@ -496,8 +518,9 @@ let private rangeTests state = .$$ (TimeSpan.MaxValue) } """ - testCaseAsync "can get range of variable with required backticks" <| - checkRanges server + testCaseAsync "can get range of variable with required backticks" + <| checkRanges + server """ module MyModule = let $D<``hello$0 world``>D$ = 42 @@ -506,23 +529,27 @@ let private rangeTests state = let _ = $<``hello world``>$ + 42 let _ = MyModule.$<``hello world``>$ + 43 """ - testCaseAsync "can get range of operator" <| + testCaseAsync "can get range of operator" + <| // Note: Parens aren't part of result range // Reason: range returned by FCS in last case (with namespace) contains opening paren, but not closing paren - checkRanges server + checkRanges + server """ let _ = 1 $0$<+>$ 2 let _ = ($<+>$) 1 2 let _ = Microsoft.FSharp.Core.Operators.($<+>$) 1 2 """ - testCaseAsync "can get range of full Active Pattern" <| + testCaseAsync "can get range of full Active Pattern" + <| // Active Pattern is strange: all together are single symbol, but each individual too // * get references on `(|Even|Odd|)` -> finds exactly `(|Even|Odd|)` // * get references on `Even` -> finds single `Even` and `Even` inside Declaration `let (|Even|Odd|)`, but not usage `(|Even|Odd|)` // // Note: Find References in FCS return range with Namespace, Module, Type -> Find Refs for `XXXX` -> range is `MyModule.XXXX` // Note: When XXXX in parens and Namespace, FCS returns range including opening paren, but NOT closing paren `MyModule.(XXXX` (happens for operators) - checkRanges server + checkRanges + server """ module MyModule = let ($D<|Ev$0en|Odd|>D$) value = @@ -544,10 +571,12 @@ let private rangeTests state = | MyModule.Even -> () | MyModule.Odd -> () """ - testCaseAsync "can get range of partial Active Pattern (Even)" <| + testCaseAsync "can get range of partial Active Pattern (Even)" + <| // Note: `Even` is found in Active Pattern declaration (`let (|Even|Odd|) = ...`) // but NOT in usage of Full Active Pattern Name (`(|Even|Odd|)`) - checkRanges server + checkRanges + server """ module MyModule = let (|$DD$|Odd|) value = @@ -569,8 +598,9 @@ let private rangeTests state = | MyModule.$$ -> () | MyModule.Odd -> () """ - testCaseAsync "can get range of type for static function call" <| - checkRanges server + testCaseAsync "can get range of type for static function call" + <| checkRanges + server """ open System open System.Threading.Tasks @@ -585,132 +615,120 @@ let private rangeTests state = .$$ .Delay (TimeSpan.MaxValue) } + """ ]) + +let tests state = + testList + "Find All References tests" + [ scriptTests state + solutionTests state + untitledTests state + rangeTests state ] + + +let tryFixupRangeTests (sourceTextFactory: ISourceTextFactory) = + testList + ($"{nameof Tokenizer.tryFixupRange}") + [ let checker = lazy (FSharpChecker.Create()) + + let getSymbolUses (source: string) cursor = + async { + let checker = checker.Value + let file = "code.fsx" + let path: string = UMX.tag file + let source = sourceTextFactory.Create(path, source) + + let! (projOptions, _) = checker.GetProjectOptionsFromScript(file, source, assumeDotNetFramework = false) + let! (parseResults, checkResults) = checker.ParseAndCheckFileInProject(file, 0, source, projOptions) + // Expect.isEmpty parseResults.Diagnostics "There should be no parse diags" + Expect.hasLength parseResults.Diagnostics 0 "There should be no parse diags" + + let checkResults = + match checkResults with + | FSharpCheckFileAnswer.Succeeded checkResults -> checkResults + | _ -> failtest "CheckFile aborted" + // Expect.isEmpty checkResults.Diagnostics "There should be no check diags" + Expect.hasLength checkResults.Diagnostics 0 "There should be no check diags" + let line = source.Lines[cursor.Line] + + let (col, idents) = + Lexer.findIdents cursor.Character line SymbolLookupKind.Fuzzy + |> Flip.Expect.wantSome "Should find idents" + + let symbolUse = + checkResults.GetSymbolUseAtLocation(cursor.Line + 1, col, line, List.ofArray idents) + |> Flip.Expect.wantSome "Should find symbol" + + let! ct = Async.CancellationToken + let usages = checkResults.GetUsesOfSymbolInFile(symbolUse.Symbol, ct) + + return (source, symbolUse.Symbol, usages) + } + + /// Markers: + /// * Cursor: `$0` + /// * Ranges: Inside `$<` ... `>$` + let extractCursorAndRanges sourceWithCursorAndRanges = + let (source, cursors) = + sourceWithCursorAndRanges + |> Text.trimTripleQuotation + |> Cursors.extractWith [| "$0"; "$<"; ">$" |] + + let (cursor, cursors) = + let (c, cs) = cursors |> List.partition (fst >> (=) "$0") + let c = c |> List.map snd + Expect.hasLength c 1 "There should be exactly 1 cursor (`$0`)" + (c[0], cs) + + let rec collectRanges cursors ranges = + match cursors with + | [] -> List.rev ranges + | ("$<", start) :: (">$", fin) :: cursors -> + let range = { Start = start; End = fin } + collectRanges cursors (range :: ranges) + | _ -> failtest $"Expected matching range pair '$<', '>$', but got: %A{cursors}" + + let ranges = collectRanges cursors [] + + (source, cursor, ranges) + + let check includeBackticks sourceWithCursorAndRanges = + async { + let (source, cursor, expected) = extractCursorAndRanges sourceWithCursorAndRanges + + let! (source, symbol, usages) = getSymbolUses source cursor + + let symbolNameCore = symbol.DisplayNameCore + + let actual = + usages + |> Seq.map (fun u -> + Tokenizer.tryFixupRange (symbolNameCore, u.Range, source, includeBackticks) + |> Option.ofValueOption + |> Flip.Expect.wantSome $"Should be able to fixup usage '%A{u}'") + |> Seq.map fcsRangeToLsp + |> Seq.toArray + |> Array.sortBy (fun r -> (r.Start.Line, r.Start.Character)) + + let expected = expected |> Array.ofList + + // Expect.equal actual expected "Should be correct range" + if actual <> expected then + // mark ranges for visual diff instead of range diff + let markRanges (ranges: Range[]) = + let locs = ranges |> Array.map (fun r -> { Uri = ""; Range = r }) + let marked = markRanges source.String locs + // append range strings for additional range diff + let rangeStrs = ranges |> Seq.map (fun r -> r.DebuggerDisplay) |> String.concat "\n" + marked + "\n" + "\n" + rangeStrs + + Expect.equal (markRanges actual) (markRanges expected) "Should be correct ranges" + } + + testCaseAsync "Active Pattern - simple" + <| check + false """ - ]) - -let tests state = testList "Find All References tests" [ - scriptTests state - solutionTests state - untitledTests state - rangeTests state -] - - -let tryFixupRangeTests (sourceTextFactoryName, sourceTextFactory : ISourceTextFactory) = testList ($"{nameof Tokenizer.tryFixupRange}.{sourceTextFactoryName}") [ - let checker = lazy (FSharpChecker.Create()) - let getSymbolUses (source : string) cursor = async { - let checker = checker.Value - let file = "code.fsx" - let path: string = UMX.tag file - let source = sourceTextFactory.Create(path, source) - - let! (projOptions, _) = checker.GetProjectOptionsFromScript(file, source, assumeDotNetFramework=false) - let! (parseResults, checkResults) = checker.ParseAndCheckFileInProject(file, 0, source, projOptions) - // Expect.isEmpty parseResults.Diagnostics "There should be no parse diags" - Expect.hasLength parseResults.Diagnostics 0 "There should be no parse diags" - let checkResults = - match checkResults with - | FSharpCheckFileAnswer.Succeeded checkResults -> checkResults - | _ -> failtest "CheckFile aborted" - // Expect.isEmpty checkResults.Diagnostics "There should be no check diags" - Expect.hasLength checkResults.Diagnostics 0 "There should be no check diags" - let line = source.Lines[cursor.Line] - let (col, idents) = - Lexer.findIdents cursor.Character line SymbolLookupKind.Fuzzy - |> Flip.Expect.wantSome "Should find idents" - let symbolUse = - checkResults.GetSymbolUseAtLocation(cursor.Line + 1, col, line, List.ofArray idents) - |> Flip.Expect.wantSome "Should find symbol" - - let! ct = Async.CancellationToken - let usages = checkResults.GetUsesOfSymbolInFile(symbolUse.Symbol, ct) - - return (source, symbolUse.Symbol, usages) - } - - /// Markers: - /// * Cursor: `$0` - /// * Ranges: Inside `$<` ... `>$` - let extractCursorAndRanges sourceWithCursorAndRanges = - let (source, cursors) = - sourceWithCursorAndRanges - |> Text.trimTripleQuotation - |> Cursors.extractWith [| "$0"; "$<"; ">$"|] - let (cursor, cursors) = - let (c, cs) = - cursors - |> List.partition (fst >> (=) "$0") - let c = c |> List.map snd - Expect.hasLength c 1 "There should be exactly 1 cursor (`$0`)" - (c[0], cs) - - let rec collectRanges cursors ranges = - match cursors with - | [] -> List.rev ranges - | ("$<", start)::(">$", fin)::cursors -> - let range = { - Start = start - End = fin - } - collectRanges cursors (range::ranges) - | _ -> - failtest $"Expected matching range pair '$<', '>$', but got: %A{cursors}" - let ranges = - collectRanges cursors [] - - (source, cursor, ranges) - - let check includeBackticks sourceWithCursorAndRanges = async { - let (source, cursor, expected) = extractCursorAndRanges sourceWithCursorAndRanges - - let! (source, symbol, usages) = getSymbolUses source cursor - - let symbolNameCore = symbol.DisplayNameCore - let actual = - usages - |> Seq.map (fun u -> - Tokenizer.tryFixupRange(symbolNameCore, u.Range, source, includeBackticks) - |> Option.ofValueOption - |> Flip.Expect.wantSome $"Should be able to fixup usage '%A{u}'" - ) - |> Seq.map fcsRangeToLsp - |> Seq.toArray - |> Array.sortBy (fun r -> (r.Start.Line, r.Start.Character)) - - let expected = expected |> Array.ofList - - // Expect.equal actual expected "Should be correct range" - if actual <> expected then - // mark ranges for visual diff instead of range diff - let markRanges (ranges: Range[]) = - let locs = - ranges - |> Array.map (fun r -> - { - Uri = "" - Range = r - } - ) - let marked = markRanges source.String locs - // append range strings for additional range diff - let rangeStrs = - ranges - |> Seq.map (fun r -> r.DebuggerDisplay) - |> String.concat "\n" - marked - + "\n" - + "\n" - + rangeStrs - - Expect.equal - (markRanges actual) - (markRanges expected) - "Should be correct ranges" - } - - testCaseAsync "Active Pattern - simple" <| - check false - """ module MyModule = let ($<|Even|Odd|>$) v = if v % 2 = 0 then Even else Odd let _ = ($<|Ev$0en|Odd|>$) 42 @@ -764,9 +782,11 @@ let tryFixupRangeTests (sourceTextFactoryName, sourceTextFactory : ISourceTextFa ) 42 """ - testCaseAsync "Active Pattern - simple - with backticks" <| - check true - """ + + testCaseAsync "Active Pattern - simple - with backticks" + <| check + true + """ module MyModule = let ($<|Even|Odd|>$) v = if v % 2 = 0 then Even else Odd let _ = ($<|Ev$0en|Odd|>$) 42 @@ -821,8 +841,9 @@ let tryFixupRangeTests (sourceTextFactoryName, sourceTextFactory : ISourceTextFa ) 42 """ - testCaseAsync "Active Pattern - required backticks" <| - check false + testCaseAsync "Active Pattern - required backticks" + <| check + false """ module MyModule = let ($<|``Hello World``|_|>$) v = Some v @@ -872,8 +893,10 @@ let tryFixupRangeTests (sourceTextFactoryName, sourceTextFactory : ISourceTextFa // ``|Hello World|_|`` // ) 42 """ - testCaseAsync "Active Pattern - required backticks - with backticks" <| - check true + + testCaseAsync "Active Pattern - required backticks - with backticks" + <| check + true """ module MyModule = let ($<|``Hello World``|_|>$) v = Some v @@ -924,8 +947,9 @@ let tryFixupRangeTests (sourceTextFactoryName, sourceTextFactory : ISourceTextFa // ) 42 """ - testCaseAsync "Active Pattern Case - simple - at usage" <| - check false + testCaseAsync "Active Pattern Case - simple - at usage" + <| check + false """ module MyModule = let (|$$|Odd|) v = @@ -951,8 +975,10 @@ let tryFixupRangeTests (sourceTextFactoryName, sourceTextFactory : ISourceTextFa | MyModule.``$$`` -> () | MyModule.``Odd`` -> () """ - testCaseAsync "Active Pattern Case - simple - at usage - with backticks" <| - check true + + testCaseAsync "Active Pattern Case - simple - at usage - with backticks" + <| check + true """ module MyModule = let (|$$|Odd|) v = @@ -979,11 +1005,13 @@ let tryFixupRangeTests (sourceTextFactoryName, sourceTextFactory : ISourceTextFa | MyModule.``Odd`` -> () """ - testCaseAsync "Active Pattern Case - simple - at decl" <| + testCaseAsync "Active Pattern Case - simple - at decl" + <| // Somehow `FSharpSymbolUse.Symbol.DisplayNameCore` is empty -- but references correct Even symbol // // Why? Cannot reproduce with just FCS -> happens just in FSAC - check false + check + false """ module MyModule = let (|$$|Odd|) v = @@ -1009,8 +1037,10 @@ let tryFixupRangeTests (sourceTextFactoryName, sourceTextFactory : ISourceTextFa | MyModule.``$$`` -> () | MyModule.``Odd`` -> () """ - testCaseAsync "Active Pattern Case - simple - at decl - with backticks" <| - check true + + testCaseAsync "Active Pattern Case - simple - at decl - with backticks" + <| check + true """ module MyModule = let (|$$|Odd|) v = @@ -1037,8 +1067,9 @@ let tryFixupRangeTests (sourceTextFactoryName, sourceTextFactory : ISourceTextFa | MyModule.``Odd`` -> () """ - testCaseAsync "operator -.-" <| - check false + testCaseAsync "operator -.-" + <| check + false """ module MyModule = let ($<-.->$) a b = a - b @@ -1078,9 +1109,12 @@ let tryFixupRangeTests (sourceTextFactoryName, sourceTextFactory : ISourceTextFa $<-.->$ ) 1 2 """ - testCaseAsync "operator -.- - with backticks" <| + + testCaseAsync "operator -.- - with backticks" + <| // same as above -- just to ensure same result - check true + check + true """ module MyModule = let ($<-.->$) a b = a - b @@ -1119,5 +1153,4 @@ let tryFixupRangeTests (sourceTextFactoryName, sourceTextFactory : ISourceTextFa ( $<-.->$ ) 1 2 - """ -] + """ ] diff --git a/test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj b/test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj index dd86a6ba2..72f195aa3 100644 --- a/test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj +++ b/test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj @@ -9,7 +9,6 @@ true true true - @@ -48,27 +47,16 @@ - + - + - - - + + - diff --git a/test/FsAutoComplete.Tests.Lsp/GoToTests.fs b/test/FsAutoComplete.Tests.Lsp/GoToTests.fs index 41c1d1598..b8eb3639c 100644 --- a/test/FsAutoComplete.Tests.Lsp/GoToTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/GoToTests.fs @@ -1,8 +1,8 @@ module FsAutoComplete.Tests.GoTo - -open Expecto +open System open System.IO +open Expecto open Helpers open Ionide.LanguageServerProtocol.Types open FsToolkit.ErrorHandling @@ -53,439 +53,440 @@ let private gotoTest state = // sequenced because we don't provide safe concurrent access to file downloads in Sourcelink.fs testSequenced <| testList - "GoTo Tests" - [ testCaseAsync - "Go-to-definition on external symbol (System.Net.HttpWebRequest)" - (async { - let! server, path, externalPath, definitionPath = server - - let p: TextDocumentPositionParams = - { TextDocument = { Uri = Path.FilePathToUri externalPath } - Position = { Line = 4; Character = 30 } } - - let! res = server.TextDocumentDefinition p - - match res with - | Result.Error e -> failtestf "Request failed: %A" e - | Result.Ok None -> failtest "Request none" - | Result.Ok (Some (GotoResult.Multiple _)) -> failtest "Should only get one location" - | Result.Ok (Some (GotoResult.Single r)) when r.Uri.EndsWith("startup") -> - failtest "Should not generate the startup dummy file" - | Result.Ok (Some (GotoResult.Single r)) -> - Expect.stringEnds r.Uri ".cs" "should have generated a C# code file" - - Expect.stringContains - r.Uri - "System.Net.HttpWebRequest" - "The generated file should be for the HttpWebRequest type" - - () // should - }) - - testCaseAsync - "Go-to-definition on external namespace (System.Net) should error when going to a namespace " - (async { - let! server, path, externalPath, definitionPath = server - - let p: TextDocumentPositionParams = - { TextDocument = { Uri = Path.FilePathToUri externalPath } - Position = { Line = 2; Character = 15 } } - - let! res = server.TextDocumentDefinition p - - match res with - | Result.Error e -> - Expect.equal "Could not find declaration" e.Message "Should report failure for navigating to a namespace" - | Result.Ok r -> failtestf "Declaration request should not work on a namespace, instead we got %A" r - }) - - testCaseAsync - "Go-to-definition" - (async { - let! server, path, externalPath, definitionPath = server - - let p: TextDocumentPositionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 2; Character = 29 } } - - let! res = server.TextDocumentDefinition p - - match res with - | Result.Error e -> failtestf "Request failed: %A" e - | Result.Ok None -> failtest "Request none" - | Result.Ok (Some res) -> - match res with - | GotoResult.Multiple _ -> failtest "Should be single GotoResult" - | GotoResult.Single res -> - Expect.stringContains res.Uri "Definition.fs" "Result should be in Definition.fs" - - Expect.equal - res.Range - { Start = { Line = 2; Character = 4 } - End = { Line = 2; Character = 16 } } - "Result should have correct range" - }) - - testCaseAsync - "Go-to-definition on custom type binding" - (async { - let! server, path, externalPath, definitionPath = server - - let p: TextDocumentPositionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 4; Character = 24 } } - - let! res = server.TextDocumentDefinition p - - match res with - | Result.Error e -> failtestf "Request failed: %A" e - | Result.Ok None -> failtest "Request none" - | Result.Ok (Some res) -> - match res with - | GotoResult.Multiple _ -> failtest "Should be single GotoResult" - | GotoResult.Single res -> - Expect.stringContains res.Uri "Definition.fs" "Result should be in Definition.fs" - - Expect.equal - res.Range - { Start = { Line = 6; Character = 4 } - End = { Line = 6; Character = 19 } } - "Result should have correct range" - }) - - ptestCaseAsync - "Go-to-implementation-on-interface-definition" - (async { - let! server, path, externalPath, definitionPath = server - - let p: TextDocumentPositionParams = - { TextDocument = { Uri = Path.FilePathToUri definitionPath } - Position = { Line = 8; Character = 11 } } - - let! res = server.TextDocumentImplementation p - - match res with - | Result.Error e -> failtestf "Request failed: %A" e - | Result.Ok None -> failtest "Request none" - | Result.Ok (Some res) -> - match res with - | GotoResult.Single res -> failtest "Should be multiple GotoResult" - | GotoResult.Multiple res -> - // TODO??? - // Expect.exists res (fun r -> r.Uri.Contains "Library.fs" && r.Range = { Start = {Line = 7; Character = 8 }; End = {Line = 7; Character = 30 }}) "First result should be in Library.fs" - // Expect.exists res (fun r -> r.Uri.Contains "Library.fs" && r.Range = { Start = {Line = 13; Character = 14 }; End = {Line = 13; Character = 36 }}) "Second result should be in Library.fs" - () - }) - - testCaseAsync - "Go-to-implementation on sourcelink file with sourcelink in PDB" - (async { - let! server, path, externalPath, definitionPath = server - - // check for the 'button' member in giraffe view engine - let p: TextDocumentPositionParams = - { TextDocument = { Uri = Path.FilePathToUri externalPath } - Position = { Line = 9; Character = 34 } } - - let! res = server.TextDocumentDefinition p - - match res with - | Result.Error e -> failtestf "Request failed: %A" e - | Result.Ok None -> failtest "Request none" - | Result.Ok (Some res) -> - match res with - | GotoResult.Multiple _ -> failtest "Should be single GotoResult" - | GotoResult.Single res -> - Expect.stringContains res.Uri "GiraffeViewEngine.fs" "Result should be in GiraffeViewEngine" - let localPath = Path.FileUriToLocalPath res.Uri - - Expect.isTrue - (System.IO.File.Exists localPath) - (sprintf "File '%s' should exist locally after being downloaded" localPath) - }) - - testCaseAsync - "Go-to-implementation on sourcelink file with sourcelink in DLL" - (async { - let! server, path, externalPath, definitionPath = server - - // check for the 'List.concat' member in FSharp.Core - let p: TextDocumentPositionParams = - { TextDocument = { Uri = Path.FilePathToUri externalPath } - Position = { Line = 12; Character = 36 } } - - let! res = server.TextDocumentDefinition p - - match res with - | Result.Error e -> failtestf "Request failed: %A" e - | Result.Ok None -> failtest "Request none" - | Result.Ok (Some res) -> - match res with - | GotoResult.Multiple _ -> failtest "Should be single GotoResult" - | GotoResult.Single res -> - Expect.stringContains res.Uri "FSharp.Core/list.fs" "Result should be in FSharp.Core's list.fs" - let localPath = Path.FileUriToLocalPath res.Uri - - Expect.isTrue - (System.IO.File.Exists localPath) - (sprintf "File '%s' should exist locally after being downloaded" localPath) - }) - - // marked pending because we don't have filename information for C# sources - ptestCaseAsync - "Go-to-implementation on C# file" - (async { - let! server, path, externalPath, definitionPath = server - - // check for the 'Stirng.Join' member in the BCL - let p: TextDocumentPositionParams = - { TextDocument = { Uri = Path.FilePathToUri externalPath } - Position = { Line = 14; Character = 79 } } - - let! res = server.TextDocumentDefinition p - - match res with - | Result.Error e -> failtestf "Request failed: %A" e - | Result.Ok None -> failtest "Request none" - | Result.Ok (Some res) -> - match res with - | GotoResult.Multiple _ -> failtest "Should be single GotoResult" - | GotoResult.Single res -> - let localPath = Path.FileUriToLocalPath res.Uri - - if - localPath.Contains - "System.String netstandard_ Version_2.0.0.0_ Culture_neutral_ PublicKeyToken_cc7b13ffcd2ddd51" then - failtestf "should not decompile when sourcelink is available" - - Expect.stringContains localPath "System.String" "Result should be in the BCL's source files" - - Expect.isTrue - (System.IO.File.Exists localPath) - (sprintf "File '%s' should exist locally after being downloaded" localPath) - }) - - testCaseAsync - "Go-to-type-definition" - (async { - let! server, path, externalPath, definitionPath = server - - let p: TextDocumentPositionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 4; Character = 24 } } - - let! res = server.TextDocumentTypeDefinition p - - match res with - | Result.Error e -> failtestf "Request failed: %A" e - | Result.Ok None -> failtest "Request none" - | Result.Ok (Some res) -> - match res with - | GotoResult.Multiple _ -> failtest "Should be single GotoResult" - | GotoResult.Single res -> - Expect.stringContains res.Uri "Definition.fs" "Result should be in Definition.fs" - - Expect.equal - res.Range - { Start = { Line = 4; Character = 5 } - End = { Line = 4; Character = 6 } } - "Result should have correct range" - }) - - testCaseAsync - "Go-to-type-definition on first char of identifier" - (async { - let! server, path, externalPath, definitionPath = server - - let p: TextDocumentPositionParams = - { TextDocument = { Uri = Path.FilePathToUri path } - Position = { Line = 4; Character = 20 } } - - let! res = server.TextDocumentTypeDefinition p - - match res with - | Result.Error e -> failtestf "Request failed: %A" e - | Result.Ok None -> failtest "Request none" - | Result.Ok (Some res) -> - match res with - | GotoResult.Multiple _ -> failtest "Should be single GotoResult" - | GotoResult.Single res -> - Expect.stringContains res.Uri "Definition.fs" "Result should be in Definition.fs" - - Expect.equal - res.Range - { Start = { Line = 4; Character = 5 } - End = { Line = 4; Character = 6 } } - "Result should have correct range" - }) - - testCaseAsync - "Go-to-type-defintion on parameter" - (async { - let! server, path, externalPath, definitionPath = server - - // check for parameter of type `'a list` -> FSharp.Core - (* + "GoTo Tests" + [ testCaseAsync + "Go-to-definition on external symbol (System.Net.HttpWebRequest)" + (async { + let! server, _path, externalPath, _definitionPath = server + + let p: TextDocumentPositionParams = + { TextDocument = { Uri = Path.FilePathToUri externalPath } + Position = { Line = 4; Character = 30 } } + + let! res = server.TextDocumentDefinition p + + match res with + | Result.Error e -> failtestf "Request failed: %A" e + | Result.Ok None -> failtest "Request none" + | Result.Ok(Some(GotoResult.Multiple _)) -> failtest "Should only get one location" + | Result.Ok(Some(GotoResult.Single r)) when r.Uri.EndsWith("startup", StringComparison.Ordinal) -> + failtest "Should not generate the startup dummy file" + | Result.Ok(Some(GotoResult.Single r)) -> + Expect.stringEnds r.Uri ".cs" "should have generated a C# code file" + + Expect.stringContains + r.Uri + "System.Net.HttpWebRequest" + "The generated file should be for the HttpWebRequest type" + + () // should + }) + + testCaseAsync + "Go-to-definition on external namespace (System.Net) should error when going to a namespace " + (async { + let! server, _path, externalPath, _definitionPath = server + + let p: TextDocumentPositionParams = + { TextDocument = { Uri = Path.FilePathToUri externalPath } + Position = { Line = 2; Character = 15 } } + + let! res = server.TextDocumentDefinition p + + match res with + | Result.Error e -> + Expect.equal "Could not find declaration" e.Message "Should report failure for navigating to a namespace" + | Result.Ok r -> failtestf "Declaration request should not work on a namespace, instead we got %A" r + }) + + testCaseAsync + "Go-to-definition" + (async { + let! server, path, _externalPath, _definitionPath = server + + let p: TextDocumentPositionParams = + { TextDocument = { Uri = Path.FilePathToUri path } + Position = { Line = 2; Character = 29 } } + + let! res = server.TextDocumentDefinition p + + match res with + | Result.Error e -> failtestf "Request failed: %A" e + | Result.Ok None -> failtest "Request none" + | Result.Ok(Some res) -> + match res with + | GotoResult.Multiple _ -> failtest "Should be single GotoResult" + | GotoResult.Single res -> + Expect.stringContains res.Uri "Definition.fs" "Result should be in Definition.fs" + + Expect.equal + res.Range + { Start = { Line = 2; Character = 4 } + End = { Line = 2; Character = 16 } } + "Result should have correct range" + }) + + testCaseAsync + "Go-to-definition on custom type binding" + (async { + let! server, path, _externalPath, _definitionPath = server + + let p: TextDocumentPositionParams = + { TextDocument = { Uri = Path.FilePathToUri path } + Position = { Line = 4; Character = 24 } } + + let! res = server.TextDocumentDefinition p + + match res with + | Result.Error e -> failtestf "Request failed: %A" e + | Result.Ok None -> failtest "Request none" + | Result.Ok(Some res) -> + match res with + | GotoResult.Multiple _ -> failtest "Should be single GotoResult" + | GotoResult.Single res -> + Expect.stringContains res.Uri "Definition.fs" "Result should be in Definition.fs" + + Expect.equal + res.Range + { Start = { Line = 6; Character = 4 } + End = { Line = 6; Character = 19 } } + "Result should have correct range" + }) + + ptestCaseAsync + "Go-to-implementation-on-interface-definition" + (async { + let! server, _path, _externalPath, definitionPath = server + + let p: TextDocumentPositionParams = + { TextDocument = { Uri = Path.FilePathToUri definitionPath } + Position = { Line = 8; Character = 11 } } + + let! res = server.TextDocumentImplementation p + + match res with + | Result.Error e -> failtestf "Request failed: %A" e + | Result.Ok None -> failtest "Request none" + | Result.Ok(Some res) -> + match res with + | GotoResult.Single _res -> failtest "Should be multiple GotoResult" + | GotoResult.Multiple _res -> + // TODO??? + // Expect.exists res (fun r -> r.Uri.Contains "Library.fs" && r.Range = { Start = {Line = 7; Character = 8 }; End = {Line = 7; Character = 30 }}) "First result should be in Library.fs" + // Expect.exists res (fun r -> r.Uri.Contains "Library.fs" && r.Range = { Start = {Line = 13; Character = 14 }; End = {Line = 13; Character = 36 }}) "Second result should be in Library.fs" + () + }) + + testCaseAsync + "Go-to-implementation on sourcelink file with sourcelink in PDB" + (async { + let! server, _path, externalPath, _definitionPath = server + + // check for the 'button' member in giraffe view engine + let p: TextDocumentPositionParams = + { TextDocument = { Uri = Path.FilePathToUri externalPath } + Position = { Line = 9; Character = 34 } } + + let! res = server.TextDocumentDefinition p + + match res with + | Result.Error e -> failtestf "Request failed: %A" e + | Result.Ok None -> failtest "Request none" + | Result.Ok(Some res) -> + match res with + | GotoResult.Multiple _ -> failtest "Should be single GotoResult" + | GotoResult.Single res -> + Expect.stringContains res.Uri "GiraffeViewEngine.fs" "Result should be in GiraffeViewEngine" + let localPath = Path.FileUriToLocalPath res.Uri + + Expect.isTrue + (System.IO.File.Exists localPath) + (sprintf "File '%s' should exist locally after being downloaded" localPath) + }) + + testCaseAsync + "Go-to-implementation on sourcelink file with sourcelink in DLL" + (async { + let! server, _path, externalPath, _definitionPath = server + + // check for the 'List.concat' member in FSharp.Core + let p: TextDocumentPositionParams = + { TextDocument = { Uri = Path.FilePathToUri externalPath } + Position = { Line = 12; Character = 36 } } + + let! res = server.TextDocumentDefinition p + + match res with + | Result.Error e -> failtestf "Request failed: %A" e + | Result.Ok None -> failtest "Request none" + | Result.Ok(Some res) -> + match res with + | GotoResult.Multiple _ -> failtest "Should be single GotoResult" + | GotoResult.Single res -> + Expect.stringContains res.Uri "FSharp.Core/list.fs" "Result should be in FSharp.Core's list.fs" + let localPath = Path.FileUriToLocalPath res.Uri + + Expect.isTrue + (System.IO.File.Exists localPath) + (sprintf "File '%s' should exist locally after being downloaded" localPath) + }) + + // marked pending because we don't have filename information for C# sources + ptestCaseAsync + "Go-to-implementation on C# file" + (async { + let! server, _path, externalPath, _definitionPath = server + + // check for the 'Stirng.Join' member in the BCL + let p: TextDocumentPositionParams = + { TextDocument = { Uri = Path.FilePathToUri externalPath } + Position = { Line = 14; Character = 79 } } + + let! res = server.TextDocumentDefinition p + + match res with + | Result.Error e -> failtestf "Request failed: %A" e + | Result.Ok None -> failtest "Request none" + | Result.Ok(Some res) -> + match res with + | GotoResult.Multiple _ -> failtest "Should be single GotoResult" + | GotoResult.Single res -> + let localPath = Path.FileUriToLocalPath res.Uri + + if + localPath.Contains + "System.String netstandard_ Version_2.0.0.0_ Culture_neutral_ PublicKeyToken_cc7b13ffcd2ddd51" + then + failtestf "should not decompile when sourcelink is available" + + Expect.stringContains localPath "System.String" "Result should be in the BCL's source files" + + Expect.isTrue + (System.IO.File.Exists localPath) + (sprintf "File '%s' should exist locally after being downloaded" localPath) + }) + + testCaseAsync + "Go-to-type-definition" + (async { + let! server, path, _externalPath, _definitionPath = server + + let p: TextDocumentPositionParams = + { TextDocument = { Uri = Path.FilePathToUri path } + Position = { Line = 4; Character = 24 } } + + let! res = server.TextDocumentTypeDefinition p + + match res with + | Result.Error e -> failtestf "Request failed: %A" e + | Result.Ok None -> failtest "Request none" + | Result.Ok(Some res) -> + match res with + | GotoResult.Multiple _ -> failtest "Should be single GotoResult" + | GotoResult.Single res -> + Expect.stringContains res.Uri "Definition.fs" "Result should be in Definition.fs" + + Expect.equal + res.Range + { Start = { Line = 4; Character = 5 } + End = { Line = 4; Character = 6 } } + "Result should have correct range" + }) + + testCaseAsync + "Go-to-type-definition on first char of identifier" + (async { + let! server, path, _externalPath, _definitionPath = server + + let p: TextDocumentPositionParams = + { TextDocument = { Uri = Path.FilePathToUri path } + Position = { Line = 4; Character = 20 } } + + let! res = server.TextDocumentTypeDefinition p + + match res with + | Result.Error e -> failtestf "Request failed: %A" e + | Result.Ok None -> failtest "Request none" + | Result.Ok(Some res) -> + match res with + | GotoResult.Multiple _ -> failtest "Should be single GotoResult" + | GotoResult.Single res -> + Expect.stringContains res.Uri "Definition.fs" "Result should be in Definition.fs" + + Expect.equal + res.Range + { Start = { Line = 4; Character = 5 } + End = { Line = 4; Character = 6 } } + "Result should have correct range" + }) + + testCaseAsync + "Go-to-type-defintion on parameter" + (async { + let! server, _path, externalPath, _definitionPath = server + + // check for parameter of type `'a list` -> FSharp.Core + (* `let myConcat listA listB = List.concat [listA; listB]` ^ position *) - let p: TextDocumentPositionParams = - { TextDocument = { Uri = Path.FilePathToUri externalPath } - Position = { Line = 12; Character = 16 } } - - let! res = server.TextDocumentTypeDefinition p - - match res with - | Result.Error e -> failtestf "Request failed: %A" e - | Result.Ok None -> failtest "Request none" - | Result.Ok (Some res) -> - match res with - | GotoResult.Multiple _ -> failtest "Should be single GotoResult" - | GotoResult.Single res -> - Expect.stringContains res.Uri "FSharp.Core/prim-types" "Result should be in FSharp.Core's prim-types" - let localPath = Path.FileUriToLocalPath res.Uri - - Expect.isTrue - (System.IO.File.Exists localPath) - (sprintf "File '%s' should exist locally after being downloaded" localPath) - }) - - testCaseAsync - "Go-to-type-defintion on variable" - (async { - let! server, path, externalPath, definitionPath = server - - // check for variable of type `System.Collections.Generic.List<_>` - (* + let p: TextDocumentPositionParams = + { TextDocument = { Uri = Path.FilePathToUri externalPath } + Position = { Line = 12; Character = 16 } } + + let! res = server.TextDocumentTypeDefinition p + + match res with + | Result.Error e -> failtestf "Request failed: %A" e + | Result.Ok None -> failtest "Request none" + | Result.Ok(Some res) -> + match res with + | GotoResult.Multiple _ -> failtest "Should be single GotoResult" + | GotoResult.Single res -> + Expect.stringContains res.Uri "FSharp.Core/prim-types" "Result should be in FSharp.Core's prim-types" + let localPath = Path.FileUriToLocalPath res.Uri + + Expect.isTrue + (System.IO.File.Exists localPath) + (sprintf "File '%s' should exist locally after being downloaded" localPath) + }) + + testCaseAsync + "Go-to-type-defintion on variable" + (async { + let! server, _path, externalPath, _definitionPath = server + + // check for variable of type `System.Collections.Generic.List<_>` + (* `let myList = System.Collections.Generic.List()` ^ position *) - let p: TextDocumentPositionParams = - { TextDocument = { Uri = Path.FilePathToUri externalPath } - Position = { Line = 16; Character = 6 } } - - let! res = server.TextDocumentTypeDefinition p - - match res with - | Result.Error e -> failtestf "Request failed: %A" e - | Result.Ok None -> failtest "Request none" - | Result.Ok (Some res) -> - match res with - | GotoResult.Multiple _ -> failtest "Should be single GotoResult" - | GotoResult.Single res -> - let localPath = Path.FileUriToLocalPath res.Uri - - Expect.stringContains - res.Uri - "System.Collections.Generic.List" - "Result should be for System.Collections.Generic.List" - - Expect.isTrue - (System.IO.File.Exists localPath) - (sprintf "File '%s' should exist locally after being downloaded" localPath) - }) - - testCaseAsync - "Go-to-type-defintion on constructor" - (async { - let! server, path, externalPath, definitionPath = server - - // check for constructor of type `System.Collections.Generic.List<_>` - (* + let p: TextDocumentPositionParams = + { TextDocument = { Uri = Path.FilePathToUri externalPath } + Position = { Line = 16; Character = 6 } } + + let! res = server.TextDocumentTypeDefinition p + + match res with + | Result.Error e -> failtestf "Request failed: %A" e + | Result.Ok None -> failtest "Request none" + | Result.Ok(Some res) -> + match res with + | GotoResult.Multiple _ -> failtest "Should be single GotoResult" + | GotoResult.Single res -> + let localPath = Path.FileUriToLocalPath res.Uri + + Expect.stringContains + res.Uri + "System.Collections.Generic.List" + "Result should be for System.Collections.Generic.List" + + Expect.isTrue + (System.IO.File.Exists localPath) + (sprintf "File '%s' should exist locally after being downloaded" localPath) + }) + + testCaseAsync + "Go-to-type-defintion on constructor" + (async { + let! server, _path, externalPath, _definitionPath = server + + // check for constructor of type `System.Collections.Generic.List<_>` + (* `let myList = System.Collections.Generic.List()` ^ position *) - let p: TextDocumentPositionParams = - { TextDocument = { Uri = Path.FilePathToUri externalPath } - Position = { Line = 16; Character = 42 } } - - let! res = server.TextDocumentTypeDefinition p - - match res with - | Result.Error e -> failtestf "Request failed: %A" e - | Result.Ok None -> failtest "Request none" - | Result.Ok (Some res) -> - match res with - | GotoResult.Multiple _ -> failtest "Should be single GotoResult" - | GotoResult.Single res -> - let localPath = Path.FileUriToLocalPath res.Uri - - Expect.stringContains - res.Uri - "System.Collections.Generic.List" - "Result should be for System.Collections.Generic.List" - - Expect.isTrue - (System.IO.File.Exists localPath) - (sprintf "File '%s' should exist locally after being downloaded" localPath) - }) - - testCaseAsync - "Go-to-type-defintion on union case" - (async { - let! server, path, externalPath, definitionPath = server - - // check for union case of type `_ option` - (* + let p: TextDocumentPositionParams = + { TextDocument = { Uri = Path.FilePathToUri externalPath } + Position = { Line = 16; Character = 42 } } + + let! res = server.TextDocumentTypeDefinition p + + match res with + | Result.Error e -> failtestf "Request failed: %A" e + | Result.Ok None -> failtest "Request none" + | Result.Ok(Some res) -> + match res with + | GotoResult.Multiple _ -> failtest "Should be single GotoResult" + | GotoResult.Single res -> + let localPath = Path.FileUriToLocalPath res.Uri + + Expect.stringContains + res.Uri + "System.Collections.Generic.List" + "Result should be for System.Collections.Generic.List" + + Expect.isTrue + (System.IO.File.Exists localPath) + (sprintf "File '%s' should exist locally after being downloaded" localPath) + }) + + testCaseAsync + "Go-to-type-defintion on union case" + (async { + let! server, _path, externalPath, _definitionPath = server + + // check for union case of type `_ option` + (* `let o v = Some v` ^ position *) - let p: TextDocumentPositionParams = - { TextDocument = { Uri = Path.FilePathToUri externalPath } - Position = { Line = 18; Character = 12 } } - - let! res = server.TextDocumentTypeDefinition p - - match res with - | Result.Error e -> failtestf "Request failed: %A" e - | Result.Ok None -> failtest "Request none" - | Result.Ok (Some res) -> - match res with - | GotoResult.Multiple _ -> failtest "Should be single GotoResult" - | GotoResult.Single res -> - Expect.stringContains res.Uri "FSharp.Core/prim-types" "Result should be in FSharp.Core's prim-types" - let localPath = Path.FileUriToLocalPath res.Uri - - Expect.isTrue - (System.IO.File.Exists localPath) - (sprintf "File '%s' should exist locally after being downloaded" localPath) - }) - - testCaseAsync - "Go-to-type-definition on property" - (async { - let! server, path, externalPath, definitionPath = server - - // check for property of type `string option` - (* + let p: TextDocumentPositionParams = + { TextDocument = { Uri = Path.FilePathToUri externalPath } + Position = { Line = 18; Character = 12 } } + + let! res = server.TextDocumentTypeDefinition p + + match res with + | Result.Error e -> failtestf "Request failed: %A" e + | Result.Ok None -> failtest "Request none" + | Result.Ok(Some res) -> + match res with + | GotoResult.Multiple _ -> failtest "Should be single GotoResult" + | GotoResult.Single res -> + Expect.stringContains res.Uri "FSharp.Core/prim-types" "Result should be in FSharp.Core's prim-types" + let localPath = Path.FileUriToLocalPath res.Uri + + Expect.isTrue + (System.IO.File.Exists localPath) + (sprintf "File '%s' should exist locally after being downloaded" localPath) + }) + + testCaseAsync + "Go-to-type-definition on property" + (async { + let! server, _path, externalPath, _definitionPath = server + + // check for property of type `string option` + (* `b.Value |> ignore` ^ position *) - let p: TextDocumentPositionParams = - { TextDocument = { Uri = Path.FilePathToUri externalPath } - Position = { Line = 24; Character = 5 } } - - let! res = server.TextDocumentTypeDefinition p - - match res with - | Result.Error e -> failtestf "Request failed: %A" e - | Result.Ok None -> failtest "Request none" - | Result.Ok (Some res) -> - match res with - | GotoResult.Multiple _ -> failtest "Should be single GotoResult" - | GotoResult.Single res -> - Expect.stringContains res.Uri "FSharp.Core/prim-types" "Result should be in FSharp.Core's prim-types" - let localPath = Path.FileUriToLocalPath res.Uri - - Expect.isTrue - (System.IO.File.Exists localPath) - (sprintf "File '%s' should exist locally after being downloaded" localPath) - }) ] + let p: TextDocumentPositionParams = + { TextDocument = { Uri = Path.FilePathToUri externalPath } + Position = { Line = 24; Character = 5 } } + + let! res = server.TextDocumentTypeDefinition p + + match res with + | Result.Error e -> failtestf "Request failed: %A" e + | Result.Ok None -> failtest "Request none" + | Result.Ok(Some res) -> + match res with + | GotoResult.Multiple _ -> failtest "Should be single GotoResult" + | GotoResult.Single res -> + Expect.stringContains res.Uri "FSharp.Core/prim-types" "Result should be in FSharp.Core's prim-types" + let localPath = Path.FileUriToLocalPath res.Uri + + Expect.isTrue + (System.IO.File.Exists localPath) + (sprintf "File '%s' should exist locally after being downloaded" localPath) + }) ] let private scriptGotoTests state = let server = @@ -502,122 +503,121 @@ let private scriptGotoTests state = // sequenced because we don't provide safe concurrent access to file downloads in Sourcelink.fs testSequenced <| testList - "Script GoTo Tests" - [ testCaseAsync - "Go-to-definition on #load integration test" - (async { - let! server, scriptPath = server - - let p: TextDocumentPositionParams = - { TextDocument = { Uri = Path.FilePathToUri scriptPath } - Position = { Line = 0; Character = 10 } } - - let! res = server.TextDocumentDefinition p - - match res with - | Error e -> failtestf "Request failed: %A" e - | Ok None -> failtest "Request none" - | Ok (Some (GotoResult.Multiple _)) -> failtest "Should only get one location" - | Ok (Some (GotoResult.Single r)) -> - Expect.stringEnds r.Uri "/simple.fsx" "should navigate to the mentioned script file" - }) - testCaseAsync - "Go-to-definition on first char of identifier works" - (async { - let! server, scriptPath = server - - let p: TextDocumentPositionParams = - { TextDocument = { Uri = Path.FilePathToUri scriptPath } - Position = { Line = 5; Character = 0 } // beginning of the usage of `testFunction` in the script file - } - - let! res = server.TextDocumentDefinition p - - match res with - | Error e -> failtestf "Request failed: %A" e - | Ok None -> failtest "Request none" - | Ok (Some (GotoResult.Multiple _)) -> failtest "Should only get one location" - | Ok (Some (GotoResult.Single r)) -> - Expect.stringEnds r.Uri (Path.GetFileName scriptPath) "should navigate to the mentioned script file" - - Expect.equal - r.Range - { Start = { Line = 3; Character = 4 } - End = { Line = 3; Character = 16 } } - "should point to the range of the definition of `testFunction`" - }) ] + "Script GoTo Tests" + [ testCaseAsync + "Go-to-definition on #load integration test" + (async { + let! server, scriptPath = server + + let p: TextDocumentPositionParams = + { TextDocument = { Uri = Path.FilePathToUri scriptPath } + Position = { Line = 0; Character = 10 } } + + let! res = server.TextDocumentDefinition p + + match res with + | Error e -> failtestf "Request failed: %A" e + | Ok None -> failtest "Request none" + | Ok(Some(GotoResult.Multiple _)) -> failtest "Should only get one location" + | Ok(Some(GotoResult.Single r)) -> + Expect.stringEnds r.Uri "/simple.fsx" "should navigate to the mentioned script file" + }) + testCaseAsync + "Go-to-definition on first char of identifier works" + (async { + let! server, scriptPath = server + + let p: TextDocumentPositionParams = + { TextDocument = { Uri = Path.FilePathToUri scriptPath } + Position = { Line = 5; Character = 0 } // beginning of the usage of `testFunction` in the script file + } + + let! res = server.TextDocumentDefinition p + + match res with + | Error e -> failtestf "Request failed: %A" e + | Ok None -> failtest "Request none" + | Ok(Some(GotoResult.Multiple _)) -> failtest "Should only get one location" + | Ok(Some(GotoResult.Single r)) -> + Expect.stringEnds r.Uri (Path.GetFileName scriptPath) "should navigate to the mentioned script file" + + Expect.equal + r.Range + { Start = { Line = 3; Character = 4 } + End = { Line = 3; Character = 16 } } + "should point to the range of the definition of `testFunction`" + }) ] let private untitledGotoTests state = - serverTestList "Untitled GoTo Tests" state defaultConfigDto None (fun server -> [ - testCaseAsync "can go to variable declaration" <| async { - let (usagePos, declRange, text) = - """ + serverTestList "Untitled GoTo Tests" state defaultConfigDto None (fun server -> + [ testCaseAsync "can go to variable declaration" + <| async { + let (usagePos, declRange, text) = + """ let $0x$0 = 1 let _ = () let a = $0x """ - |> Text.trimTripleQuotation - |> Cursor.assertExtractRange - |> fun (decl, text) -> - let (pos, text) = - text - |> Cursor.assertExtractPosition - (pos, decl, text) - let! (doc, diags) = server |> Server.createUntitledDocument text - - use doc = doc - - let p : TextDocumentPositionParams = { - TextDocument = doc.TextDocumentIdentifier - Position = usagePos + |> Text.trimTripleQuotation + |> Cursor.assertExtractRange + |> fun (decl, text) -> + let (pos, text) = text |> Cursor.assertExtractPosition + (pos, decl, text) + + let! (doc, _diags) = server |> Server.createUntitledDocument text + + use doc = doc + + let p: TextDocumentPositionParams = + { TextDocument = doc.TextDocumentIdentifier + Position = usagePos } + + let! res = doc.Server.Server.TextDocumentDefinition p + + match res with + | Error e -> failtestf "Request failed: %A" e + | Ok None -> failtest "Request none" + | Ok(Some(GotoResult.Multiple _)) -> failtest "Should only get one location" + | Ok(Some(GotoResult.Single r)) -> + Expect.stringEnds r.Uri doc.Uri "should navigate to source file" + Expect.equal r.Range declRange "should point to the range of variable declaration" } - let! res = doc.Server.Server.TextDocumentDefinition p - match res with - | Error e -> failtestf "Request failed: %A" e - | Ok None -> failtest "Request none" - | Ok (Some (GotoResult.Multiple _)) -> failtest "Should only get one location" - | Ok (Some (GotoResult.Single r)) -> - Expect.stringEnds r.Uri doc.Uri "should navigate to source file" - Expect.equal r.Range declRange "should point to the range of variable declaration" - } - testCaseAsync "can go to function declaration" <| async { - let (usagePos, declRange, text) = - """ + testCaseAsync "can go to function declaration" + <| async { + let (usagePos, declRange, text) = + """ let $0myFun$0 a b = a + b let _ = () let a = my$0Fun 1 1 """ - |> Text.trimTripleQuotation - |> Cursor.assertExtractRange - |> fun (decl, text) -> - let (pos, text) = - text - |> Cursor.assertExtractPosition - (pos, decl, text) - let! (doc, diags) = server |> Server.createUntitledDocument text - use doc = doc - - let p : TextDocumentPositionParams = { - TextDocument = doc.TextDocumentIdentifier - Position = usagePos - } - let! res = doc.Server.Server.TextDocumentDefinition p - match res with - | Error e -> failtestf "Request failed: %A" e - | Ok None -> failtest "Request none" - | Ok (Some (GotoResult.Multiple _)) -> failtest "Should only get one location" - | Ok (Some (GotoResult.Single r)) -> - Expect.stringEnds r.Uri doc.Uri "should navigate to source file" - Expect.equal r.Range declRange "should point to the range of function declaration" - } - ]) + |> Text.trimTripleQuotation + |> Cursor.assertExtractRange + |> fun (decl, text) -> + let (pos, text) = text |> Cursor.assertExtractPosition + (pos, decl, text) + + let! (doc, _diags) = server |> Server.createUntitledDocument text + use doc = doc + + let p: TextDocumentPositionParams = + { TextDocument = doc.TextDocumentIdentifier + Position = usagePos } + + let! res = doc.Server.Server.TextDocumentDefinition p + + match res with + | Error e -> failtestf "Request failed: %A" e + | Ok None -> failtest "Request none" + | Ok(Some(GotoResult.Multiple _)) -> failtest "Should only get one location" + | Ok(Some(GotoResult.Single r)) -> + Expect.stringEnds r.Uri doc.Uri "should navigate to source file" + Expect.equal r.Range declRange "should point to the range of function declaration" + } ]) let tests createServer = testSequenced <| testList - "Go to definition tests" - [ - gotoTest createServer - scriptGotoTests createServer - untitledGotoTests createServer - ] + "Go to definition tests" + [ gotoTest createServer + scriptGotoTests createServer + untitledGotoTests createServer ] diff --git a/test/FsAutoComplete.Tests.Lsp/Helpers.fs b/test/FsAutoComplete.Tests.Lsp/Helpers.fs index a6da8adbd..2b0ee4c6b 100644 --- a/test/FsAutoComplete.Tests.Lsp/Helpers.fs +++ b/test/FsAutoComplete.Tests.Lsp/Helpers.fs @@ -14,35 +14,35 @@ open FSharp.UMX module Expecto = open System.Threading.Tasks - let inline testBuilderWithTimeout (ts : TimeSpan) name testCase focus = - TestLabel(name, TestCase (Test.timeout (int ts.TotalMilliseconds) (testCase), focus), focus) - - let inline testCaseWithTimeout (ts : TimeSpan) name test = - testBuilderWithTimeout ts name (Sync test) Normal - let inline ftestCaseWithTimeout (ts : TimeSpan) name test = - testBuilderWithTimeout ts name (Sync test) Focused - let inline ptestCaseWithTimeout (ts : TimeSpan) name test = - testBuilderWithTimeout ts name (Sync test) Pending - - let inline testCaseAsyncWithTimeout (ts : TimeSpan) name test = - testBuilderWithTimeout ts name (Async test) Normal - let inline ftestCaseAsyncWithTimeout (ts : TimeSpan) name test = - testBuilderWithTimeout ts name (Async test) Focused - let inline ptestCaseAsyncWithTimeout (ts : TimeSpan) name test = - testBuilderWithTimeout ts name (Async test) Pending - - let inline testCaseTaskWithTimeout (ts : TimeSpan) name (test : unit -> Task) = + + let inline testBuilderWithTimeout (ts: TimeSpan) name testCase focus = + TestLabel(name, TestCase(Test.timeout (int ts.TotalMilliseconds) (testCase), focus), focus) + + let inline testCaseWithTimeout (ts: TimeSpan) name test = testBuilderWithTimeout ts name (Sync test) Normal + let inline ftestCaseWithTimeout (ts: TimeSpan) name test = testBuilderWithTimeout ts name (Sync test) Focused + let inline ptestCaseWithTimeout (ts: TimeSpan) name test = testBuilderWithTimeout ts name (Sync test) Pending + + let inline testCaseAsyncWithTimeout (ts: TimeSpan) name test = testBuilderWithTimeout ts name (Async test) Normal + let inline ftestCaseAsyncWithTimeout (ts: TimeSpan) name test = testBuilderWithTimeout ts name (Async test) Focused + let inline ptestCaseAsyncWithTimeout (ts: TimeSpan) name test = testBuilderWithTimeout ts name (Async test) Pending + + let inline testCaseTaskWithTimeout (ts: TimeSpan) name (test: unit -> Task) = testCaseAsyncWithTimeout ts name (async.Delay(fun () -> Async.AwaitTask(test ()))) - let inline ftestCaseTaskWithTimeout (ts : TimeSpan) name (test : unit -> Task) = + + let inline ftestCaseTaskWithTimeout (ts: TimeSpan) name (test: unit -> Task) = ftestCaseAsyncWithTimeout ts name (async.Delay(fun () -> Async.AwaitTask(test ()))) - let inline ptestCaseTaskWithTimeout (ts : TimeSpan) name (test : unit -> Task) = + + let inline ptestCaseTaskWithTimeout (ts: TimeSpan) name (test: unit -> Task) = ptestCaseAsyncWithTimeout ts name (async.Delay(fun () -> Async.AwaitTask(test ()))) // millisecond let DEFAULT_TIMEOUT = Environment.GetEnvironmentVariable "FSAC_TEST_DEFAULT_TIMEOUT" |> Option.ofObj - |> Option.bind(fun x -> match Int32.TryParse x with | (true, v) -> Some v | _ -> None ) + |> Option.bind (fun x -> + match Int32.TryParse x with + | (true, v) -> Some v + | _ -> None) |> Option.defaultValue (60000) |> TimeSpan.FromMilliseconds @@ -159,17 +159,16 @@ type Async = /// previous execution. static member Cache(input: Async<'T>) = let agent = - MailboxProcessor>.Start - (fun agent -> - async { + MailboxProcessor>.Start(fun agent -> + async { + let! repl = agent.Receive() + let! res = input |> Async.Catch + repl.Reply(res) + + while true do let! repl = agent.Receive() - let! res = input |> Async.Catch repl.Reply(res) - - while true do - let! repl = agent.Receive() - repl.Reply(res) - }) + }) async { let! result = agent.PostAndAsyncReply(id) @@ -231,6 +230,8 @@ let defaultConfigDto: FSharpConfigDto = EnableReferenceCodeLens = None EnableAnalyzers = None AnalyzersPath = None + ExcludeAnalyzers = None + IncludeAnalyzers = None DisableInMemoryProjectReferences = None AutomaticWorkspaceInit = Some true InterfaceStubGeneration = None @@ -343,9 +344,7 @@ let clientCaps: ClientCapabilities = CompletionItemKind = Some cikCaps ContextSupport = Some true InsertTextMode = Some InsertTextMode.AsIs - CompletionList = Some { - ItemDefaults = None - } } + CompletionList = Some { ItemDefaults = None } } let hoverCaps: HoverCapabilities = { DynamicRegistration = Some true @@ -368,7 +367,7 @@ let clientCaps: ClientCapabilities = SymbolKind = Some skCaps HierarchicalDocumentSymbolSupport = Some false TagSupport = None - LabelSupport = Some true } + LabelSupport = Some true } let foldingRangeCaps: FoldingRangeCapabilities = { DynamicRegistration = Some true @@ -403,7 +402,7 @@ let clientCaps: ClientCapabilities = { DynamicRegistration = Some true ResolveSupport = None } - let inlineValueCaps: InlineValueClientCapabilities = + let _inlineValueCaps: InlineValueClientCapabilities = { DynamicRegistration = Some true ResolveSupport = None } @@ -430,16 +429,12 @@ let clientCaps: ClientCapabilities = LinkSupport = Some false } let docLinkCaps: DocumentLinkCapabilities = - { - DynamicRegistration = Some true - TooltipSupport = Some true - } + { DynamicRegistration = Some true + TooltipSupport = Some true } let diagCaps: DiagnosticCapabilities = - { - DynamicRegistration = Some true - RelatedDocumentSupport = Some true - } + { DynamicRegistration = Some true + RelatedDocumentSupport = Some true } { Synchronization = Some syncCaps PublishDiagnostics = Some publishDiagCaps @@ -470,8 +465,7 @@ let clientCaps: ClientCapabilities = LinkedEditingRange = Some dynCaps Moniker = Some dynCaps InlineValue = Some dynCaps - Diagnostic = Some diagCaps - } + Diagnostic = Some diagCaps } { Workspace = Some workspaceCaps TextDocument = Some textCaps @@ -486,8 +480,7 @@ open FsAutoComplete.CommandResponse open CliWrap open CliWrap.Buffered -let logEvent (name, payload) = - logger.Value.Debug("{name}: {payload}", name, payload) +let logEvent (name, payload) = logger.Value.Debug("{name}: {payload}", name, payload) let logDotnetRestore section line = if not (String.IsNullOrWhiteSpace(line)) then @@ -504,15 +497,12 @@ let runProcess (workingDir: string) (exePath: string) (args: string) = let! ctok = Async.CancellationToken let! result = - Cli.Wrap(exePath).WithArguments(args).WithWorkingDirectory( - workingDir - ) - .WithValidation( - CommandResultValidation.None - ) - .ExecuteBufferedAsync( - ctok - ) + Cli + .Wrap(exePath) + .WithArguments(args) + .WithWorkingDirectory(workingDir) + .WithValidation(CommandResultValidation.None) + .ExecuteBufferedAsync(ctok) .Task |> Async.AwaitTask @@ -525,15 +515,17 @@ let inline expectExitCodeZero (r: BufferedCommandResult) = 0 $"Expected exit code zero but was %i{r.ExitCode}.\nStdOut: %s{r.StandardOutput}\nStdErr: %s{r.StandardError}" -let dotnetRestore dir = async { - let! r = runProcess (DirectoryInfo(dir).FullName) "dotnet" "restore -v d" - return expectExitCodeZero r -} +let dotnetRestore dir = + async { + let! r = runProcess (DirectoryInfo(dir).FullName) "dotnet" "restore -v d" + return expectExitCodeZero r + } -let dotnetToolRestore dir = async { - let! r = runProcess (DirectoryInfo(dir).FullName) "dotnet" "tool restore" - return expectExitCodeZero r -} +let dotnetToolRestore dir = + async { + let! r = runProcess (DirectoryInfo(dir).FullName) "dotnet" "tool restore" + return expectExitCodeZero r + } let serverInitialize path (config: FSharpConfigDto) createServer = async { @@ -565,10 +557,10 @@ let serverInitialize path (config: FSharpConfigDto) createServer = let! result = server.Initialize p match result with - | Result.Ok res -> + | Result.Ok _res -> do! server.Initialized(InitializedParams()) return (server, clientNotifications) - | Result.Error e -> return failwith "Initialization failed" + | Result.Error _e -> return failwith "Initialization failed" } let loadDocument path : TextDocumentItem = @@ -600,8 +592,8 @@ let waitForWorkspaceFinishedParsing (events: ClientEvents) = match name with | "fsharp/notifyWorkspace" -> match unbox payload with - | (UnwrappedPlainNotification "workspaceLoad" - (workspaceLoadResponse: FsAutoComplete.CommandResponse.WorkspaceLoadResponse)) -> + | (UnwrappedPlainNotification "workspaceLoad" (workspaceLoadResponse: + FsAutoComplete.CommandResponse.WorkspaceLoadResponse)) -> if workspaceLoadResponse.Status = "finished" then Some() else @@ -619,13 +611,10 @@ let waitForWorkspaceFinishedParsing (events: ClientEvents) = let private typedEvents<'t> (typ: string) : IObservable -> IObservable<'t> = Observable.choose (fun (typ', _o) -> if typ' = typ then Some(unbox _o) else None) -let private payloadAs<'t> = Observable.map (fun (_typ, o) -> unbox<'t> o) - let private getDiagnosticsEvents: IObservable -> IObservable<_> = typedEvents "textDocument/publishDiagnostics" -let private fileName (u: DocumentUri) = - u.Split([| '/'; '\\' |], StringSplitOptions.RemoveEmptyEntries) |> Array.last +let private fileName (u: DocumentUri) = u.Split([| '/'; '\\' |], StringSplitOptions.RemoveEmptyEntries) |> Array.last /// note that the files here are intended to be the filename only., not the full URI. let private matchFiles (files: string Set) = @@ -670,40 +659,38 @@ let fileDiagnosticsForUri (uri: string) = let diagnosticsFromSource (desiredSource: String) = Observable.choose (fun (diags: Diagnostic[]) -> - match diags - |> Array.choose (fun d -> - match d.Source with - | Some s -> if s.StartsWith desiredSource then Some d else None - | None -> None) - with + match + diags + |> Array.choose (fun d -> + match d.Source with + | Some s -> + if s.StartsWith(desiredSource, StringComparison.Ordinal) then + Some d + else + None + | None -> None) + with | [||] -> None | diags -> Some diags) -let analyzerDiagnostics file = - fileDiagnostics file >> diagnosticsFromSource "F# Analyzers" +let analyzerDiagnostics file = fileDiagnostics file >> diagnosticsFromSource "F# Analyzers" -let linterDiagnostics file = - fileDiagnostics file >> diagnosticsFromSource "F# Linter" +let linterDiagnostics file = fileDiagnostics file >> diagnosticsFromSource "F# Linter" -let fsacDiagnostics file = - fileDiagnostics file >> diagnosticsFromSource "FSAC" +let fsacDiagnostics file = fileDiagnostics file >> diagnosticsFromSource "FSAC" -let compilerDiagnostics file = - fileDiagnostics file >> diagnosticsFromSource "F# Compiler" +let compilerDiagnostics file = fileDiagnostics file >> diagnosticsFromSource "F# Compiler" let diagnosticsToResult = Observable.map (function | [||] -> Ok() | diags -> Core.Error diags) -let waitForParseResultsForFile file = - fileDiagnostics file >> diagnosticsToResult >> Async.AwaitObservable +let waitForParseResultsForFile file = fileDiagnostics file >> diagnosticsToResult >> Async.AwaitObservable -let waitForFsacDiagnosticsForFile file = - fsacDiagnostics file >> diagnosticsToResult >> Async.AwaitObservable +let waitForFsacDiagnosticsForFile file = fsacDiagnostics file >> diagnosticsToResult >> Async.AwaitObservable -let waitForCompilerDiagnosticsForFile file = - compilerDiagnostics file >> diagnosticsToResult >> Async.AwaitObservable +let waitForCompilerDiagnosticsForFile file = compilerDiagnostics file >> diagnosticsToResult >> Async.AwaitObservable let waitForParsedScript (event: ClientEvents) = event @@ -721,8 +708,7 @@ let waitForTestDetected (fileName: string) (events: ClientEvents) : Async Async.AwaitObservable -let waitForEditsForFile file = - workspaceEdits >> editsFor file >> Async.AwaitObservable +let waitForEditsForFile file = workspaceEdits >> editsFor file >> Async.AwaitObservable let trySerialize (t: string) : 't option = try @@ -732,7 +718,7 @@ let trySerialize (t: string) : 't option = let (|As|_|) (m: PlainNotification) : 't option = match trySerialize m.Content with - | Some (r: FsAutoComplete.CommandResponse.ResponseMsg<'t>) -> Some r.Data + | Some(r: FsAutoComplete.CommandResponse.ResponseMsg<'t>) -> Some r.Data | None -> None let (|CodeActions|_|) (t: TextDocumentCodeActionResult) = diff --git a/test/FsAutoComplete.Tests.Lsp/HighlightingTests.fs b/test/FsAutoComplete.Tests.Lsp/HighlightingTests.fs index 3d1d40c59..78c93951a 100644 --- a/test/FsAutoComplete.Tests.Lsp/HighlightingTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/HighlightingTests.fs @@ -20,18 +20,16 @@ let tests state = do! server.TextDocumentDidOpen tdop match! waitForParseResultsForFile "Script.fsx" event with - | Ok () -> return server + | Ok() -> return server | Error errors -> let errorStrings = - errors - |> Array.map (fun e -> e.DebuggerDisplay) - |> String.concat "\n\t* " + errors |> Array.map (fun e -> e.DebuggerDisplay) |> String.concat "\n\t* " return failtestf "Errors while parsing highlighting script:\n\t* %s" errorStrings } |> Async.Cache - let decodeHighlighting (data: uint32 []) = + let decodeHighlighting (data: uint32[]) = let zeroLine = [| 0u; 0u; 0u; 0u; 0u |] let lines = Array.append [| zeroLine |] (Array.chunkBySize 5 data) @@ -76,7 +74,7 @@ let tests state = let! highlights = server.TextDocumentSemanticTokensFull p match highlights with - | Ok (Some highlights) -> + | Ok(Some highlights) -> let decoded = highlights.Data |> decodeHighlighting // printfn "%A" decoded return decoded @@ -94,7 +92,7 @@ let tests state = let tokenIsOfType ((line, char) as pos) testTokenType - (highlights: (Range * ClassificationUtils.SemanticTokenTypes * ClassificationUtils.SemanticTokenModifier) [] Async) + (highlights: (Range * ClassificationUtils.SemanticTokenTypes * ClassificationUtils.SemanticTokenModifier)[] Async) = testCaseAsync $"can find token of type {testTokenType} at %A{pos}" @@ -109,7 +107,7 @@ let tests state = }) /// this tests the range endpoint by getting highlighting for a range then doing the normal highlighting test - let tokenIsOfTypeInRange ((startLine, startChar), (endLine, endChar)) ((line, char)) testTokenType = + let _tokenIsOfTypeInRange ((startLine, startChar), (endLine, endChar)) ((line, char)) testTokenType = testCaseAsync $"can find token of type {testTokenType} in a subrange from ({startLine}, {startChar})-({endLine}, {endChar})" (async { @@ -127,8 +125,8 @@ let tests state = server.TextDocumentSemanticTokensRange { Range = range TextDocument = { Uri = Path.FilePathToUri scriptPath } } - with - | Ok (Some highlights) -> + with + | Ok(Some highlights) -> let decoded = decodeHighlighting highlights.Data Expect.exists diff --git a/test/FsAutoComplete.Tests.Lsp/InfoPanelTests.fs b/test/FsAutoComplete.Tests.Lsp/InfoPanelTests.fs index ffb21047b..078535b59 100644 --- a/test/FsAutoComplete.Tests.Lsp/InfoPanelTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/InfoPanelTests.fs @@ -20,7 +20,7 @@ let docFormattingTest state = do! waitForParseResultsForFile "Script.fsx" events - |> AsyncResult.bimap id (fun e -> failtest "should have not had check errors") + |> AsyncResult.bimap id (fun _e -> failtest "should have not had check errors") return (server, path) } @@ -40,7 +40,7 @@ let docFormattingTest state = match doc with | Result.Error err -> failtest $"Doc error: {err.Message}" - | Result.Ok (Some(As (model: FsAutoComplete.CommandResponse.DocumentationDescription ))) -> + | Result.Ok(Some(As(model: FsAutoComplete.CommandResponse.DocumentationDescription))) -> Expect.stringContains model.Signature "'Key, 'U" "Formatted doc contains both params separated by (, )" | Result.Ok _ -> failtest "couldn't parse doc as the json type we expected" }) @@ -57,7 +57,7 @@ let docFormattingTest state = match doc with | Result.Error err -> failtest $"Doc error: {err.Message}" - | Result.Ok (Some (As (model: FsAutoComplete.CommandResponse.DocumentationDescription))) -> + | Result.Ok(Some(As(model: FsAutoComplete.CommandResponse.DocumentationDescription))) -> Expect.stringContains model.Signature "'T1 * 'T2 * 'T3" "Formatted doc contains 3 params separated by ( * )" | Result.Ok _ -> failtest "couldn't parse doc as the json type we expected" }) ] diff --git a/test/FsAutoComplete.Tests.Lsp/InlayHintTests.fs b/test/FsAutoComplete.Tests.Lsp/InlayHintTests.fs index 6b8a6e331..433757289 100644 --- a/test/FsAutoComplete.Tests.Lsp/InlayHintTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/InlayHintTests.fs @@ -147,7 +147,7 @@ module private LspInlayHints = (server: CachedServer) (text: string) (range: Range) - (validateInlayHints: Document -> string -> InlayHint[] -> Async) + (validateInlayHints: string -> InlayHint[] -> Async) = async { let! (doc, diags) = server |> Server.createUntitledDocument text @@ -156,11 +156,10 @@ module private LspInlayHints = let! hints = doc |> Document.inlayHintsAt range let hints = hints |> Array.sortBy (fun h -> h.Position) - do! validateInlayHints doc text hints + do! validateInlayHints text hints } let private validateHint - (doc: Document) (expectedBase: InlayHint) (textAfterEdits: string option) (text: string) @@ -232,12 +231,12 @@ module private LspInlayHints = let hint = { hint with Position = cursor } (hint, textAfterEdits)) - let validateHints doc (text: string) (hints: InlayHint[]) = + let validateHints (text: string) (hints: InlayHint[]) = async { Expect.hasLength hints expected.Length "Number of actual hints and expected hints don't match" for (actual, (expected, textAfterEdits)) in Seq.zip hints expected do - do! validateHint doc expected textAfterEdits text actual + do! validateHint expected textAfterEdits text actual } do! checkInRange server text range validateHints @@ -1642,7 +1641,7 @@ open FSharp.UMX open FsAutoComplete.LspHelpers open Ionide.LanguageServerProtocol.Types -let explicitTypeInfoTests (sourceTextFactoryName, sourceTextFactory: ISourceTextFactory) = +let explicitTypeInfoTests (sourceTextFactory: ISourceTextFactory) = let file = "test.fsx" let checker = lazy (FSharpChecker.Create()) @@ -1798,7 +1797,7 @@ let explicitTypeInfoTests (sourceTextFactoryName, sourceTextFactory: ISourceText testSequenced <| testList - $"detect type and parens.{sourceTextFactoryName}" + $"detect type and parens" [ testList "Expr" [ testList diff --git a/test/FsAutoComplete.Tests.Lsp/InlineHintsTests.fs b/test/FsAutoComplete.Tests.Lsp/InlineHintsTests.fs new file mode 100644 index 000000000..17bc613bd --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/InlineHintsTests.fs @@ -0,0 +1,69 @@ +module FsAutoComplete.Tests.InlineHints + +open Expecto +open Helpers +open Ionide.LanguageServerProtocol.Types +open FsAutoComplete +open Utils.Server +open Utils.Utils +open Utils.TextEdit +open Utils.ServerTests +open Helpers.Expecto.ShadowedTimeouts + +let private testInlineHints (server : CachedServer) text checkResp = + async { + let range, text = + text + |> Text.trimTripleQuotation + |> Cursor.assertExtractRange + + let! doc, _diags = server |> Server.createUntitledDocument text + use doc = doc + + let inlineValueRequest: InlineValueParams = { + Context = { + FrameId = 0 + StoppedLocation = range + } + Range = range + TextDocument = doc.TextDocumentIdentifier + } + let! resp = doc.Server.Server.TextDocumentInlineValue inlineValueRequest + checkResp resp + } + +let good = """ +let test value = + $0 + value + |> String.replicate 3 + |> String.filter (fun c -> Char.IsAscii(c)) + |> String.length + $0 +value "foo" +""" + +let tests state = + serverTestList "inline hints" state defaultConfigDto None (fun server -> [ + testCaseAsync + "Gets inline hints for simple pipeline" + (testInlineHints + server + good + (fun resp -> + Expect.isOk resp "should get inline hints for valid code fragment" + let resp = resp |> Result.defaultWith (fun _ -> failwith "Unpossible!") + Expect.isSome resp "should get inline hints for valid code fragment" + let resp = resp |> Option.get + Expect.hasLength resp 4 "THERE ARE FOUR LIGHTS!!" + let hints = + resp + |> Array.choose ( + function + | InlineValue.InlineValueText hint -> Some hint.Text + | _ -> None + ) + Expect.containsAll hints [nameof string; nameof int] "should be 3 strings and an int" + ) + ) + ]) diff --git a/test/FsAutoComplete.Tests.Lsp/Program.fs b/test/FsAutoComplete.Tests.Lsp/Program.fs index 437c073ad..9e09c51e9 100644 --- a/test/FsAutoComplete.Tests.Lsp/Program.fs +++ b/test/FsAutoComplete.Tests.Lsp/Program.fs @@ -32,23 +32,16 @@ let testTimeout = Environment.SetEnvironmentVariable("FSAC_WORKSPACELOAD_DELAY", "250") let loaders = - [ - "Ionide WorkspaceLoader", (fun toolpath -> WorkspaceLoader.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) + [ "Ionide WorkspaceLoader", + (fun toolpath -> WorkspaceLoader.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) // "MSBuild Project Graph WorkspaceLoader", (fun toolpath -> WorkspaceLoaderViaProjectGraph.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) - ] + ] let adaptiveLspServerFactory toolsPath workspaceLoaderFactory sourceTextFactory = Helpers.createAdaptiveServer (fun () -> workspaceLoaderFactory toolsPath) sourceTextFactory -let lspServers = - [ - "AdaptiveLspServer", adaptiveLspServerFactory - ] - -let sourceTextFactories: (string * ISourceTextFactory) list = [ - "RoslynSourceText", RoslynSourceTextFactory() -] +let sourceTextFactory: ISourceTextFactory = RoslynSourceTextFactory() let mutable toolsPath = Ionide.ProjInfo.Init.init (System.IO.DirectoryInfo Environment.CurrentDirectory) None @@ -57,78 +50,70 @@ let lspTests = testList "lsp" [ for (loaderName, workspaceLoaderFactory) in loaders do - for (lspName, lspFactory) in lspServers do - for (sourceTextName, sourceTextFactory) in sourceTextFactories do - - testList - $"{loaderName}.{lspName}.{sourceTextName}" - [ - Templates.tests () - let createServer () = - lspFactory toolsPath workspaceLoaderFactory sourceTextFactory - - initTests createServer - closeTests createServer - - Utils.Tests.Server.tests createServer - Utils.Tests.CursorbasedTests.tests createServer - - CodeLens.tests createServer - documentSymbolTest createServer - Completion.autocompleteTest createServer - Completion.autoOpenTests createServer - Completion.fullNameExternalAutocompleteTest createServer - foldingTests createServer - tooltipTests createServer - Highlighting.tests createServer - scriptPreviewTests createServer - scriptEvictionTests createServer - scriptProjectOptionsCacheTests createServer - dependencyManagerTests createServer - interactiveDirectivesUnitTests - - // commented out because FSDN is down - //fsdnTest createServer - - //linterTests createServer - uriTests - formattingTests createServer - analyzerTests createServer - signatureTests createServer - SignatureHelp.tests createServer - CodeFixTests.Tests.tests sourceTextFactory createServer - Completion.tests createServer - GoTo.tests createServer - - FindReferences.tests createServer - Rename.tests createServer - - InfoPanelTests.docFormattingTest createServer - DetectUnitTests.tests createServer - XmlDocumentationGeneration.tests createServer - InlayHintTests.tests createServer - DependentFileChecking.tests createServer - UnusedDeclarationsTests.tests createServer - EmptyFileTests.tests createServer - CallHierarchy.tests createServer - ] ] + + testList + $"{loaderName}" + [ + Templates.tests () + let createServer () = + adaptiveLspServerFactory toolsPath workspaceLoaderFactory sourceTextFactory + + initTests createServer + closeTests createServer + + Utils.Tests.Server.tests createServer + Utils.Tests.CursorbasedTests.tests createServer + + CodeLens.tests createServer + documentSymbolTest createServer + Completion.autocompleteTest createServer + Completion.autoOpenTests createServer + Completion.fullNameExternalAutocompleteTest createServer + foldingTests createServer + tooltipTests createServer + Highlighting.tests createServer + scriptPreviewTests createServer + scriptEvictionTests createServer + scriptProjectOptionsCacheTests createServer + dependencyManagerTests createServer + interactiveDirectivesUnitTests + + // commented out because FSDN is down + //fsdnTest createServer + + //linterTests createServer + uriTests + formattingTests createServer + analyzerTests createServer + signatureTests createServer + SignatureHelp.tests createServer + InlineHints.tests createServer + CodeFixTests.Tests.tests sourceTextFactory createServer + Completion.tests createServer + GoTo.tests createServer + + FindReferences.tests createServer + Rename.tests createServer + + InfoPanelTests.docFormattingTest createServer + DetectUnitTests.tests createServer + XmlDocumentationGeneration.tests createServer + InlayHintTests.tests createServer + DependentFileChecking.tests createServer + UnusedDeclarationsTests.tests createServer + EmptyFileTests.tests createServer + CallHierarchy.tests createServer + ] ] /// Tests that do not require a LSP server let generalTests = testList "general" [ testList (nameof (Utils)) [ Utils.Tests.Utils.tests; Utils.Tests.TextEdit.tests ] - for (name, factory) in sourceTextFactories do - InlayHintTests.explicitTypeInfoTests (name, factory) - FindReferences.tryFixupRangeTests (name, factory) + InlayHintTests.explicitTypeInfoTests sourceTextFactory + FindReferences.tryFixupRangeTests sourceTextFactory ] [] -let tests = - testList - "FSAC" - [ - generalTests - lspTests - ] +let tests = testList "FSAC" [ generalTests; lspTests ] [] @@ -142,10 +127,10 @@ let main args = let logLevel = match args - |> Array.tryFind (fun arg -> arg.StartsWith logMarker) + |> Array.tryFind (fun arg -> arg.StartsWith(logMarker, StringComparison.Ordinal)) |> Option.map (fun log -> log.Substring(logMarker.Length)) with - | Some ("warn" | "warning") -> Logging.LogLevel.Warn + | Some("warn" | "warning") -> Logging.LogLevel.Warn | Some "error" -> Logging.LogLevel.Error | Some "fatal" -> Logging.LogLevel.Fatal | Some "info" -> Logging.LogLevel.Info @@ -155,7 +140,7 @@ let main args = let args = args - |> Array.filter (fun arg -> not <| arg.StartsWith logMarker) + |> Array.filter (fun arg -> not <| arg.StartsWith(logMarker, StringComparison.Ordinal)) logLevel, args @@ -173,10 +158,12 @@ let main args = let toExclude = args - |> Array.filter (fun arg -> arg.StartsWith excludeMarker) + |> Array.filter (fun arg -> arg.StartsWith(excludeMarker, StringComparison.Ordinal)) |> Array.collect (fun arg -> arg.Substring(excludeMarker.Length).Split(',')) - let args = args |> Array.filter (fun arg -> not <| arg.StartsWith excludeMarker) + let args = + args + |> Array.filter (fun arg -> not <| arg.StartsWith(excludeMarker, StringComparison.Ordinal)) toExclude, args @@ -190,7 +177,7 @@ let main args = fun s -> s <> null && logSourcesToExclude |> Array.contains s ) - let argsToRemove, loaders = + let argsToRemove, _loaders = args |> Array.windowed 2 |> Array.tryPick (function @@ -207,10 +194,8 @@ let main args = .Filter.ByExcluding(Matching.FromSource("FileSystem")) .Filter.ByExcluding(sourcesToExclude) - .Destructure - .FSharpTypes() - .Destructure - .ByTransforming(fun r -> + .Destructure.FSharpTypes() + .Destructure.ByTransforming(fun r -> box {| FileName = r.FileName Start = r.Start @@ -218,8 +203,7 @@ let main args = .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 -> + .WriteTo.Async(fun c -> c.Console( outputTemplate = outputTemplate, standardErrorFromLevel = Nullable<_>(LogEventLevel.Verbose), @@ -236,13 +220,10 @@ let main args = let cts = new CancellationTokenSource(testTimeout) - let args = - [ - CLIArguments.Printer (Expecto.Impl.TestPrinters.summaryWithLocationPrinter defaultConfig.printer) + let args = + [ CLIArguments.Printer(Expecto.Impl.TestPrinters.summaryWithLocationPrinter defaultConfig.printer) CLIArguments.Verbosity logLevel // CLIArguments.Parallel - ] + ] runTestsWithCLIArgsAndCancel cts.Token args fixedUpArgs tests - - diff --git a/test/FsAutoComplete.Tests.Lsp/RenameTests.fs b/test/FsAutoComplete.Tests.Lsp/RenameTests.fs index e8f410c51..e9ba9e8b7 100644 --- a/test/FsAutoComplete.Tests.Lsp/RenameTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/RenameTests.fs @@ -14,9 +14,7 @@ open Utils.TextEdit open Helpers.Expecto.ShadowedTimeouts let private normalizePathCasing = - Path.FilePathToUri - >> Path.FileUriToLocalPath - >> Path.FilePathToUri + Path.FilePathToUri >> Path.FileUriToLocalPath >> Path.FilePathToUri let private sameProjectTests state = let testDir = @@ -40,7 +38,7 @@ let private sameProjectTests state = match res with | Result.Error e -> failtestf "Request failed: %A" e | Result.Ok None -> failtest "Request none" - | Result.Ok (Some res) -> + | Result.Ok(Some res) -> match res.DocumentChanges with | None -> failtest "No changes" | Some result -> @@ -51,9 +49,9 @@ let private sameProjectTests state = (fun n -> n.TextDocument.Uri.Contains "Program.fs" && n.Edits - |> Seq.exists (fun r -> - r.Range = { Start = { Line = 7; Character = 12 } - End = { Line = 7; Character = 13 } })) + |> Seq.exists (fun r -> + r.Range = { Start = { Line = 7; Character = 12 } + End = { Line = 7; Character = 13 } })) "Rename contains changes in Program.fs" Expect.exists @@ -61,9 +59,9 @@ let private sameProjectTests state = (fun n -> n.TextDocument.Uri.Contains "Test.fs" && n.Edits - |> Seq.exists (fun r -> - r.Range = { Start = { Line = 2; Character = 4 } - End = { Line = 2; Character = 5 } })) + |> Seq.exists (fun r -> + r.Range = { Start = { Line = 2; Character = 4 } + End = { Line = 2; Character = 5 } })) "Rename contains changes in Test.fs" () @@ -73,7 +71,7 @@ let private sameProjectTests state = "Rename from definition within the same project with usages across different files" (async { - let! (programDoc, programDiags) = server |> Server.openDocument "Program.fs" + let! (_programDoc, programDiags) = server |> Server.openDocument "Program.fs" let! (testDoc, testDiags) = server |> Server.openDocument "Test.fs" Expect.isEmpty programDiags "There should be no errors in Program.fs" @@ -90,7 +88,7 @@ let private sameProjectTests state = match res with | Result.Error e -> failtestf "Request failed: %A" e | Result.Ok None -> failtest "Request none" - | Result.Ok (Some res) -> + | Result.Ok(Some res) -> match res.DocumentChanges with | None -> failtest "No changes" | Some result -> @@ -102,9 +100,9 @@ let private sameProjectTests state = (fun n -> n.TextDocument.Uri.Contains "Program.fs" && n.Edits - |> Seq.exists (fun r -> - r.Range = { Start = { Line = 7; Character = 12 } - End = { Line = 7; Character = 13 } })) + |> Seq.exists (fun r -> + r.Range = { Start = { Line = 7; Character = 12 } + End = { Line = 7; Character = 13 } })) "Rename contains changes in Program.fs" Expect.exists @@ -112,9 +110,9 @@ let private sameProjectTests state = (fun n -> n.TextDocument.Uri.Contains "Test.fs" && n.Edits - |> Seq.exists (fun r -> - r.Range = { Start = { Line = 2; Character = 4 } - End = { Line = 2; Character = 5 } })) + |> Seq.exists (fun r -> + r.Range = { Start = { Line = 2; Character = 4 } + End = { Line = 2; Character = 5 } })) "Rename contains changes in Test.fs" () @@ -123,57 +121,56 @@ let private sameProjectTests state = ]) let private sameScriptTests state = - serverTestList "Within same script file" state defaultConfigDto None (fun server -> [ - /// `expectedText = None` -> Rename not valid at location - let checkRename' textWithCursor newName (expectedText: string option) = async { - let (cursor, text) = - textWithCursor - |> Text.trimTripleQuotation - |> Cursor.assertExtractPosition - - let! (doc, diags) = server |> Server.createUntitledDocument text - use doc = doc - Expect.isEmpty diags "There should be no diags" - - let p: RenameParams = - { - TextDocument = doc.TextDocumentIdentifier - Position = cursor - NewName = newName - } - let! res = doc.Server.Server.TextDocumentRename p - match expectedText with - | None -> - // Note: `Error` instead of `Ok None` -> error message - Expect.isError res "Rename should not be valid!" - | Some expectedText -> - let edits = - match res with - | Result.Error e -> failtestf "Request failed: %A" e - | Result.Ok None -> failtest "Request none" - | Result.Ok (Some { DocumentChanges = Some edits }) -> edits - | Result.Ok edits -> failtestf "got some unexpected edits: %A" edits + serverTestList "Within same script file" state defaultConfigDto None (fun server -> + [ /// `expectedText = None` -> Rename not valid at location + let checkRename' textWithCursor newName (expectedText: string option) = + async { + let (cursor, text) = + textWithCursor |> Text.trimTripleQuotation |> Cursor.assertExtractPosition - Expect.hasLength edits 1 "should have just one file worth of edits" - let edit = edits.[0] - Expect.equal edit.TextDocument.Uri doc.Uri "should be for this file" - let edits = edit.Edits |> List.ofArray |> TextEdits.sortByRange - let actual = - text - |> TextEdits.applyWithErrorCheck edits - |> Flip.Expect.wantOk "TextEdits should be valid" + let! (doc, diags) = server |> Server.createUntitledDocument text + use doc = doc + Expect.isEmpty diags "There should be no diags" - let expected = expectedText |> Text.trimTripleQuotation + let p: RenameParams = + { TextDocument = doc.TextDocumentIdentifier + Position = cursor + NewName = newName } - Expect.equal actual expected "Text after TextEdits should be correct" - } - let checkRename textWithCursor newName expectedText = - checkRename' textWithCursor newName (Some expectedText) - let checkRenameNotValid textWithCursor newName = - checkRename' textWithCursor newName None + let! res = doc.Server.Server.TextDocumentRename p + + match expectedText with + | None -> + // Note: `Error` instead of `Ok None` -> error message + Expect.isError res "Rename should not be valid!" + | Some expectedText -> + let edits = + match res with + | Result.Error e -> failtestf "Request failed: %A" e + | Result.Ok None -> failtest "Request none" + | Result.Ok(Some { DocumentChanges = Some edits }) -> edits + | Result.Ok edits -> failtestf "got some unexpected edits: %A" edits + + Expect.hasLength edits 1 "should have just one file worth of edits" + let edit = edits.[0] + Expect.equal edit.TextDocument.Uri doc.Uri "should be for this file" + let edits = edit.Edits |> List.ofArray |> TextEdits.sortByRange + + let actual = + text + |> TextEdits.applyWithErrorCheck edits + |> Flip.Expect.wantOk "TextEdits should be valid" + + let expected = expectedText |> Text.trimTripleQuotation + + Expect.equal actual expected "Text after TextEdits should be correct" + } - testCaseAsync "Rename from definition within script file" <| - checkRename + let checkRename textWithCursor newName expectedText = checkRename' textWithCursor newName (Some expectedText) + let checkRenameNotValid textWithCursor newName = checkRename' textWithCursor newName None + + testCaseAsync "Rename from definition within script file" + <| checkRename """ let $0initial () = printfn "hi" @@ -185,8 +182,9 @@ let private sameScriptTests state = afterwards () """ - testCaseAsync "Rename from usage within script file" <| - checkRename + + testCaseAsync "Rename from usage within script file" + <| checkRename """ let initial () = printfn "hi" @@ -198,8 +196,9 @@ let private sameScriptTests state = afterwards () """ - testCaseAsync "can add backticks to new name with space" <| - checkRename + + testCaseAsync "can add backticks to new name with space" + <| checkRename """ let initial () = printfn "hi" @@ -211,8 +210,9 @@ let private sameScriptTests state = ``hello world`` () """ - testCaseAsync "doesn't add additional backticks to new name with backticks" <| - checkRename + + testCaseAsync "doesn't add additional backticks to new name with backticks" + <| checkRename """ let initial () = printfn "hi" @@ -225,8 +225,8 @@ let private sameScriptTests state = ``hello world`` () """ - testCaseAsync "can rename operator to valid operator name" <| - checkRename + testCaseAsync "can rename operator to valid operator name" + <| checkRename """ let (+$0++) a b = a + b """ @@ -234,15 +234,16 @@ let private sameScriptTests state = """ let (---) a b = a + b """ - testCaseAsync "cannot rename operator to invalid operator name" <| - checkRenameNotValid + + testCaseAsync "cannot rename operator to invalid operator name" + <| checkRenameNotValid """ let (+$0++) a b = a + b """ "foo" - testCaseAsync "removes backticks for new name without backticks" <| - checkRename + testCaseAsync "removes backticks for new name without backticks" + <| checkRename """ let ``my $0value`` = 42 @@ -253,8 +254,7 @@ let private sameScriptTests state = let value = 42 let _ = value + 42 - """ - ]) + """ ]) let private crossProjectTests state = let server = @@ -270,320 +270,338 @@ let private crossProjectTests state = testSequenced <| testList - "Across projects" - [ testCaseAsync - "Rename from usage across projects" - (async { - let! (server, rootDir, events) = server - let declarationFile = Path.Combine(rootDir, "LibA", "Library.fs") - let usageFile = Path.Combine(rootDir, "LibB", "Library.fs") - - // open and parse the usage file - let tdop: DidOpenTextDocumentParams = { TextDocument = loadDocument usageFile } - do! server.TextDocumentDidOpen tdop - - do! - waitForParseResultsForFile (Path.GetFileName usageFile) events - |> AsyncResult.foldResult id (fun e -> failtestf "%A" e) - - // now, request renames - let renameHelloUsageInUsageFile: RenameParams = - { TextDocument = { Uri = normalizePathCasing usageFile } - Position = { Line = 6; Character = 28 } - NewName = "sup" } - - let! res = server.TextDocumentRename(renameHelloUsageInUsageFile) + "Across projects" + [ testCaseAsync + "Rename from usage across projects" + (async { + let! (server, rootDir, events) = server + let _declarationFile = Path.Combine(rootDir, "LibA", "Library.fs") + let usageFile = Path.Combine(rootDir, "LibB", "Library.fs") - match res with - | Result.Error e -> failtest $"Expected to get renames, but got error: {e.Message}" - | Result.Ok None -> failtest $"Expected to get renames, but got none" - | Result.Ok (Some { DocumentChanges = Some edits }) -> - Expect.equal edits.Length 2 "Rename has the correct expected edits" - - Expect.exists - edits - (fun n -> - n.TextDocument.Uri.Contains "LibA" - && n.TextDocument.Uri.Contains "Library.fs" - && n.Edits - |> Seq.exists (fun r -> - r.Range = { Start = { Line = 3; Character = 8 } - End = { Line = 3; Character = 13 } } - && r.NewText = "sup")) - "Rename contains changes in LibA" - - Expect.exists - edits - (fun n -> - n.TextDocument.Uri.Contains "LibB" - && n.TextDocument.Uri.Contains "Library.fs" - && n.Edits - |> Seq.exists (fun r -> - r.Range = { Start = { Line = 6; Character = 28 } - End = { Line = 6; Character = 33 } } - && r.NewText = "sup")) - "Rename contains changes in LibB" - | Result.Ok edits -> failtestf "got some unexpected edits: %A" edits - }) - testCaseAsync - "Rename where there are fully-qualified usages" - (async { - let! (server, rootDir, events) = server - let declarationFile = Path.Combine(rootDir, "LibA", "Library.fs") - let usageFile = Path.Combine(rootDir, "LibB", "Library.fs") - - // open and parse the usage file - let tdop: DidOpenTextDocumentParams = { TextDocument = loadDocument usageFile } - do! server.TextDocumentDidOpen tdop - - do! - waitForParseResultsForFile (Path.GetFileName usageFile) events - |> AsyncResult.foldResult id (fun e -> failtestf "%A" e) - - // now, request renames - let renameHelloUsageInUsageFile: RenameParams = - { TextDocument = { Uri = normalizePathCasing usageFile } - Position = { Line = 9; Character = 37 } // in the 'yell' part of 'A.Say.yell' - NewName = "sup" } - - let! res = server.TextDocumentRename(renameHelloUsageInUsageFile) + // open and parse the usage file + let tdop: DidOpenTextDocumentParams = { TextDocument = loadDocument usageFile } + do! server.TextDocumentDidOpen tdop - match res with - | Result.Error e -> failtest $"Expected to get renames, but got error: {e.Message}" - | Result.Ok None -> failtest $"Expected to get renames, but got none" - | Result.Ok (Some { DocumentChanges = Some edits }) -> - Expect.equal edits.Length 2 "Rename has the correct expected edits" - - Expect.exists - edits - (fun n -> - n.TextDocument.Uri.Contains "LibA" - && n.TextDocument.Uri.Contains "Library.fs" - && n.Edits - |> Seq.exists (fun r -> - r.Range = { Start = { Line = 6; Character = 8 } - End = { Line = 6; Character = 12 } } - && r.NewText = "sup")) - $"Rename contains changes in LibA in the list %A{edits}" - - Expect.exists - edits - (fun n -> - n.TextDocument.Uri.Contains "LibB" - && n.TextDocument.Uri.Contains "Library.fs" - && n.Edits - |> Seq.exists (fun r -> - r.Range = { Start = { Line = 9; Character = 37 } - End = { Line = 9; Character = 41 } } - && r.NewText = "sup")) - $"Rename contains changes in LibB in the list %A{edits}" - | Result.Ok edits -> failtestf "got some unexpected edits: %A" edits - }) ] - -let private prepareRenameTests state = serverTestList "Prepare Rename tests" state defaultConfigDto None (fun server -> [ - let check shouldBeAbleToRename sourceWithCursor = async { - let (cursor, text) = - sourceWithCursor - |> Text.trimTripleQuotation - |> Cursor.assertExtractPosition - let! (doc, diags) = server |> Server.createUntitledDocument text - - let p: PrepareRenameParams = { - TextDocument = doc.TextDocumentIdentifier - Position = cursor - } + do! + waitForParseResultsForFile (Path.GetFileName usageFile) events + |> AsyncResult.foldResult id (fun e -> failtestf "%A" e) - let! res = doc.Server.Server.TextDocumentPrepareRename p - - if shouldBeAbleToRename then - res - |> Flip.Expect.wantOk "Should be able to rename" - |> Flip.Expect.isSome "Should be able to rename" - else - // Note: we always want `Error` instead of `Ok None` because of Error message - Expect.isError res "Should not be able to rename" - } - let checkCanRename = check true - let checkCannotRename = check false - - testCaseAsync "can rename variable at decl" <| - checkCanRename - """ + // now, request renames + let renameHelloUsageInUsageFile: RenameParams = + { TextDocument = { Uri = normalizePathCasing usageFile } + Position = { Line = 6; Character = 28 } + NewName = "sup" } + + let! res = server.TextDocumentRename(renameHelloUsageInUsageFile) + + match res with + | Result.Error e -> failtest $"Expected to get renames, but got error: {e.Message}" + | Result.Ok None -> failtest $"Expected to get renames, but got none" + | Result.Ok(Some { DocumentChanges = Some edits }) -> + Expect.equal edits.Length 2 "Rename has the correct expected edits" + + Expect.exists + edits + (fun n -> + n.TextDocument.Uri.Contains "LibA" + && n.TextDocument.Uri.Contains "Library.fs" + && n.Edits + |> Seq.exists (fun r -> + r.Range = { Start = { Line = 3; Character = 8 } + End = { Line = 3; Character = 13 } } + && r.NewText = "sup")) + "Rename contains changes in LibA" + + Expect.exists + edits + (fun n -> + n.TextDocument.Uri.Contains "LibB" + && n.TextDocument.Uri.Contains "Library.fs" + && n.Edits + |> Seq.exists (fun r -> + r.Range = { Start = { Line = 6; Character = 28 } + End = { Line = 6; Character = 33 } } + && r.NewText = "sup")) + "Rename contains changes in LibB" + | Result.Ok edits -> failtestf "got some unexpected edits: %A" edits + }) + testCaseAsync + "Rename where there are fully-qualified usages" + (async { + let! (server, rootDir, events) = server + let _declarationFile = Path.Combine(rootDir, "LibA", "Library.fs") + let usageFile = Path.Combine(rootDir, "LibB", "Library.fs") + + // open and parse the usage file + let tdop: DidOpenTextDocumentParams = { TextDocument = loadDocument usageFile } + do! server.TextDocumentDidOpen tdop + + do! + waitForParseResultsForFile (Path.GetFileName usageFile) events + |> AsyncResult.foldResult id (fun e -> failtestf "%A" e) + + // now, request renames + let renameHelloUsageInUsageFile: RenameParams = + { TextDocument = { Uri = normalizePathCasing usageFile } + Position = { Line = 9; Character = 37 } // in the 'yell' part of 'A.Say.yell' + NewName = "sup" } + + let! res = server.TextDocumentRename(renameHelloUsageInUsageFile) + + match res with + | Result.Error e -> failtest $"Expected to get renames, but got error: {e.Message}" + | Result.Ok None -> failtest $"Expected to get renames, but got none" + | Result.Ok(Some { DocumentChanges = Some edits }) -> + Expect.equal edits.Length 2 "Rename has the correct expected edits" + + Expect.exists + edits + (fun n -> + n.TextDocument.Uri.Contains "LibA" + && n.TextDocument.Uri.Contains "Library.fs" + && n.Edits + |> Seq.exists (fun r -> + r.Range = { Start = { Line = 6; Character = 8 } + End = { Line = 6; Character = 12 } } + && r.NewText = "sup")) + $"Rename contains changes in LibA in the list %A{edits}" + + Expect.exists + edits + (fun n -> + n.TextDocument.Uri.Contains "LibB" + && n.TextDocument.Uri.Contains "Library.fs" + && n.Edits + |> Seq.exists (fun r -> + r.Range = { Start = { Line = 9; Character = 37 } + End = { Line = 9; Character = 41 } } + && r.NewText = "sup")) + $"Rename contains changes in LibB in the list %A{edits}" + | Result.Ok edits -> failtestf "got some unexpected edits: %A" edits + }) ] + +let private prepareRenameTests state = + serverTestList "Prepare Rename tests" state defaultConfigDto None (fun server -> + [ let check shouldBeAbleToRename sourceWithCursor = + async { + let (cursor, text) = + sourceWithCursor |> Text.trimTripleQuotation |> Cursor.assertExtractPosition + + let! (doc, _diags) = server |> Server.createUntitledDocument text + + let p: PrepareRenameParams = + { TextDocument = doc.TextDocumentIdentifier + Position = cursor } + + let! res = doc.Server.Server.TextDocumentPrepareRename p + + if shouldBeAbleToRename then + res + |> Flip.Expect.wantOk "Should be able to rename" + |> Flip.Expect.isSome "Should be able to rename" + else + // Note: we always want `Error` instead of `Ok None` because of Error message + Expect.isError res "Should not be able to rename" + } + + let checkCanRename = check true + let checkCannotRename = check false + + testCaseAsync "can rename variable at decl" + <| checkCanRename + """ let val$0ue = 42 let _ = value + 42 """ - testCaseAsync "can rename variable at usage" <| - checkCanRename - """ + + testCaseAsync "can rename variable at usage" + <| checkCanRename + """ let value = 42 let _ = val$0ue + 42 """ - testCaseAsync "can rename unnecessarily backticked variable at decl" <| - checkCanRename - """ + testCaseAsync "can rename unnecessarily backticked variable at decl" + <| checkCanRename + """ let ``val$0ue`` = 42 let _ = value + 42 """ - testCaseAsync "can rename unnecessarily backticked variable at usage" <| - checkCanRename - """ + + testCaseAsync "can rename unnecessarily backticked variable at usage" + <| checkCanRename + """ let value = 42 let _ = ``val$0ue`` + 42 """ - testCaseAsync "can rename variable with required backticks at decl" <| - checkCanRename - """ + testCaseAsync "can rename variable with required backticks at decl" + <| checkCanRename + """ let ``my v$0alue`` = 42 let _ = ``my value`` + 42 """ - testCaseAsync "can rename variable with required backticks at usage" <| - checkCanRename - """ + + testCaseAsync "can rename variable with required backticks at usage" + <| checkCanRename + """ let ``my value`` = 42 let _ = ``my va$0lue`` + 42 """ - testCaseAsync "can rename function at decl" <| - checkCanRename - """ + testCaseAsync "can rename function at decl" + <| checkCanRename + """ let myFun$0ction value = value + 42 myFunction 42 |> ignore """ - testCaseAsync "can rename function at usage" <| - checkCanRename - """ + + testCaseAsync "can rename function at usage" + <| checkCanRename + """ let myFunction value = value + 42 myFun$0ction 42 |> ignore """ - testCaseAsync "can rename function parameter at decl" <| - checkCanRename - """ + + testCaseAsync "can rename function parameter at decl" + <| checkCanRename + """ let myFunction va$0lue = value + 42 myFunction 42 |> ignore """ - testCaseAsync "can rename function parameter at usage" <| - checkCanRename - """ + + testCaseAsync "can rename function parameter at usage" + <| checkCanRename + """ let myFunction va$0lue = value + 42 myFunction 42 |> ignore """ - testCaseAsync "Can rename Type at decl" <| - checkCanRename - """ + testCaseAsync "Can rename Type at decl" + <| checkCanRename + """ type MyT$0ype () = class end let v: MyType = MyType() """ - testCaseAsync "Can rename Type at instantiation usage" <| - checkCanRename - """ + + testCaseAsync "Can rename Type at instantiation usage" + <| checkCanRename + """ type MyType () = class end let v: MyType = My$0Type() """ - testCaseAsync "Can rename Type at Type usage" <| - checkCanRename - """ + + testCaseAsync "Can rename Type at Type usage" + <| checkCanRename + """ type MyType () = class end let v: MyT$0ype = MyType() """ - testCaseAsync "Can rename Method at decl" <| - checkCanRename - """ - type MyType = + testCaseAsync "Can rename Method at decl" + <| checkCanRename + """ + type MyType = static member DoS$0tuff () = () MyType.DoStuff () """ - testCaseAsync "Can rename Method at usage" <| - checkCanRename - """ - type MyType = + + testCaseAsync "Can rename Method at usage" + <| checkCanRename + """ + type MyType = static member DoStuff () = () MyType.DoS$0tuff () """ - - testCaseAsync "cannot rename Active Pattern at decl" <| - checkCannotRename - """ - let (|Ev$0en|Odd|) v = + + testCaseAsync "cannot rename Active Pattern at decl" + <| checkCannotRename + """ + let (|Ev$0en|Odd|) v = if v % 2 = 0 then Even else Odd let _ = (|Even|Odd|) 42 """ - testCaseAsync "cannot rename Active Pattern at usage" <| - checkCannotRename - """ - let (|Even|Odd|) v = + + testCaseAsync "cannot rename Active Pattern at usage" + <| checkCannotRename + """ + let (|Even|Odd|) v = if v % 2 = 0 then Even else Odd let _ = (|Ev$0en|Odd|) 42 """ - testCaseAsync "cannot rename Active Pattern Case at decl" <| - checkCannotRename - """ - let (|Ev$0en|Odd|) v = + + testCaseAsync "cannot rename Active Pattern Case at decl" + <| checkCannotRename + """ + let (|Ev$0en|Odd|) v = if v % 2 = 0 then Ev$0en else Odd match 42 with | Even -> () | Odd -> () """ - testCaseAsync "cannot rename Active Pattern Case at usage" <| - checkCannotRename - """ - let (|Even|Odd|) v = + + testCaseAsync "cannot rename Active Pattern Case at usage" + <| checkCannotRename + """ + let (|Even|Odd|) v = if v % 2 = 0 then Ev$0en else Odd match 42 with | Ev$0en -> () | Odd -> () """ - testCaseAsync "cannot rename external function" <| - checkCannotRename - """ + testCaseAsync "cannot rename external function" + <| checkCannotRename + """ 42 |> igno$0re """ - testCaseAsync "cannot rename external method" <| - checkCannotRename - """ + + testCaseAsync "cannot rename external method" + <| checkCannotRename + """ String.IsNullOr$0WhiteSpace "foo" |> ignore """ - testCaseAsync "cannot rename number" <| - checkCannotRename - """ + + testCaseAsync "cannot rename number" + <| checkCannotRename + """ 4$02 |> ignore """ - testCaseAsync "cannot rename string" <| - checkCannotRename - """ + + testCaseAsync "cannot rename string" + <| checkCannotRename + """ "hel$0lo world" |> ignore """ - testCaseAsync "cannot rename keyword" <| - checkCannotRename - """ + + testCaseAsync "cannot rename keyword" + <| checkCannotRename + """ l$0et foo = 42 """ - testCaseAsync "cannot rename comment" <| - checkCannotRename - """ + + testCaseAsync "cannot rename comment" + <| checkCannotRename + """ /// So$0me value let foo = 42 """ - testCaseAsync "cannot rename space" <| - checkCannotRename - """ + + testCaseAsync "cannot rename space" + <| checkCannotRename + """ let foo = 42 - $0 + $0 let bar = 42 - """ -]) + """ ]) let tests state = - testList "Rename Tests" [ - sameProjectTests state - sameScriptTests state - crossProjectTests state + testList + "Rename Tests" + [ sameProjectTests state + sameScriptTests state + crossProjectTests state - prepareRenameTests state - ] + prepareRenameTests state ] diff --git a/test/FsAutoComplete.Tests.Lsp/ScriptTests.fs b/test/FsAutoComplete.Tests.Lsp/ScriptTests.fs index 5e2b40527..cea2d7ebe 100644 --- a/test/FsAutoComplete.Tests.Lsp/ScriptTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/ScriptTests.fs @@ -21,7 +21,11 @@ let scriptPreviewTests state = let scriptPath = Path.Combine(path, "Script.fsx") let! (server, events) = - serverInitialize path { defaultConfigDto with FSIExtraParameters = Some [| "--langversion:preview" |] } state + serverInitialize + path + { defaultConfigDto with + FSIExtraParameters = Some [| "--langversion:preview" |] } + state do! waitForWorkspaceFinishedParsing events return server, events, scriptPath @@ -39,7 +43,7 @@ let scriptPreviewTests state = do! server.TextDocumentDidOpen { TextDocument = loadDocument scriptPath } match! waitForParseResultsForFile "Script.fsx" events with - | Ok () -> () // all good, no parsing/checking errors + | Ok() -> () // all good, no parsing/checking errors | Core.Result.Error errors -> failwithf "Errors while parsing script %s: %A" scriptPath errors }) ] ] @@ -64,8 +68,7 @@ let scriptEvictionTests state = (async { let! server, events, scriptPath = server - let openScript () = - server.TextDocumentDidOpen { TextDocument = loadDocument scriptPath } + let openScript () = server.TextDocumentDidOpen { TextDocument = loadDocument scriptPath } do! openScript () @@ -75,7 +78,10 @@ let scriptEvictionTests state = let configChange: DidChangeConfigurationParams = let config: FSharpConfigRequest = - { FSharp = Some { defaultConfigDto with FSIExtraParameters = Some [| "--nowarn:760" |] } } + { FSharp = + Some + { defaultConfigDto with + FSIExtraParameters = Some [| "--nowarn:760" |] } } { Settings = Server.serialize config } @@ -97,7 +103,7 @@ let dependencyManagerTests state = let workingDir = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "DependencyManagement") - let dependencyManagerAssemblyDir = + let _dependencyManagerAssemblyDir = Path.Combine( __SOURCE_DIRECTORY__, "..", @@ -108,7 +114,8 @@ let dependencyManagerTests state = ) let dependencyManagerEnabledConfig = - { defaultConfigDto with FSIExtraParameters = Some [| "--langversion:preview" |] } + { defaultConfigDto with + FSIExtraParameters = Some [| "--langversion:preview" |] } let! (server, events) = serverInitialize workingDir dependencyManagerEnabledConfig state do! waitForWorkspaceFinishedParsing events @@ -156,7 +163,8 @@ let scriptProjectOptionsCacheTests state = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "ScriptProjectOptsCache") let previewEnabledConfig = - { defaultConfigDto with FSIExtraParameters = Some [| "--langversion:preview" |] } + { defaultConfigDto with + FSIExtraParameters = Some [| "--langversion:preview" |] } let! (server, events) = serverInitialize workingDir previewEnabledConfig state let options = ResizeArray() @@ -177,7 +185,7 @@ let scriptProjectOptionsCacheTests state = [ testCaseAsync "reopening an unchanged script file should return same project options for file" (async { - let! server, events, workingDir, testFilePath, allOpts = server + let! server, _events, _workingDir, testFilePath, allOpts = server do! server.TextDocumentDidOpen { TextDocument = loadDocument testFilePath } do! Async.Sleep(TimeSpan.FromSeconds 3.) do! server.TextDocumentDidOpen { TextDocument = loadDocument testFilePath } diff --git a/test/FsAutoComplete.Tests.Lsp/SignatureHelpTests.fs b/test/FsAutoComplete.Tests.Lsp/SignatureHelpTests.fs index 32174ed80..f4eca713c 100644 --- a/test/FsAutoComplete.Tests.Lsp/SignatureHelpTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/SignatureHelpTests.fs @@ -16,7 +16,7 @@ type private TriggerType = let private testSignatureHelp' (server: CachedServer) text pos triggerType checkResp = async { - let! (doc, diags) = server |> Server.createUntitledDocument text + let! (doc, _diags) = server |> Server.createUntitledDocument text use doc = doc let sigHelpRequest: SignatureHelpParams = @@ -27,7 +27,7 @@ let private testSignatureHelp' (server: CachedServer) text pos triggerType check { TriggerKind = match triggerType with | Manual -> SignatureHelpTriggerKind.Invoked - | Char c -> SignatureHelpTriggerKind.TriggerCharacter + | Char _c -> SignatureHelpTriggerKind.TriggerCharacter TriggerCharacter = match triggerType with | Manual -> None @@ -45,9 +45,7 @@ let private wantSignatureHelp = Flip.Expect.wantOk "unexpected request error" let private testSignatureHelp (server: CachedServer) textWithCursor triggerType checkResp = async { let (pos, text) = - textWithCursor - |> Text.trimTripleQuotation - |> Cursor.assertExtractPosition + textWithCursor |> Text.trimTripleQuotation |> Cursor.assertExtractPosition let checkResp = wantSignatureHelp >> checkResp return! testSignatureHelp' server text pos triggerType checkResp @@ -148,17 +146,15 @@ let private overloadEdgeCasesTests server = testCaseAsync $"Can get overloads at whitespace position {c - 37} of unattached parens" <| testSignatureHelp' - server - text - pos - Manual - (wantSignatureHelp - >> fun resp -> - Expect.isSome - resp - $"Should get some signature overloads at position %A{pos} on file Overloads.fsx" - - Expect.isNonEmpty resp.Value.Signatures "Should have some overloads") ] + server + text + pos + Manual + (wantSignatureHelp + >> fun resp -> + Expect.isSome resp $"Should get some signature overloads at position %A{pos} on file Overloads.fsx" + + Expect.isNonEmpty resp.Value.Signatures "Should have some overloads") ] testList "attached parens" [ let text = "let _____ = new System.IO.MemoryStream(42)" @@ -168,17 +164,15 @@ let private overloadEdgeCasesTests server = testCaseAsync $"Can get overloads at whitespace position {c - 39} of attached parens" <| testSignatureHelp' - server - text - pos - Manual - (wantSignatureHelp - >> fun resp -> - Expect.isSome - resp - $"Should get some signature overloads at position %A{pos} on file Overloads.fsx" - - Expect.isNonEmpty resp.Value.Signatures "Should have some overloads") ] ] + server + text + pos + Manual + (wantSignatureHelp + >> fun resp -> + Expect.isSome resp $"Should get some signature overloads at position %A{pos} on file Overloads.fsx" + + Expect.isNonEmpty resp.Value.Signatures "Should have some overloads") ] ] let issuesTests server = testList @@ -201,9 +195,7 @@ let issuesTests server = "val count: x: int -> int" "Should have no backticks because signatures are only ever rendered in `code` form") testCaseAsync "issue #1040" // IndexOutOfRangeException - <| testSignatureHelp server "().ToString(\n\n,$0\n)" Manual (fun sigs -> - Expect.isSome sigs "Should have sigs" - ) ] + <| testSignatureHelp server "().ToString(\n\n,$0\n)" Manual (fun sigs -> Expect.isSome sigs "Should have sigs") ] let tests state = serverTestList "signature help" state defaultConfigDto None (fun server -> diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/Directory.Build.props b/test/FsAutoComplete.Tests.Lsp/TestCases/Directory.Build.props new file mode 100644 index 000000000..e728bc6dc --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/Directory.Build.props @@ -0,0 +1,5 @@ + + + $(NoWarn);1182 + + diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/Tooltips/Script.fsx b/test/FsAutoComplete.Tests.Lsp/TestCases/Tooltips/Script.fsx index 2ff1265ca..1844e333c 100644 --- a/test/FsAutoComplete.Tests.Lsp/TestCases/Tooltips/Script.fsx +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/Tooltips/Script.fsx @@ -53,3 +53,20 @@ let mailbox = type DiscUnionWithCaseOfLabeledTuple = | Case1 | Case2 of string * newlineBefore: bool * newlineAfter: bool + +open FSharp.Quotations.Patterns + +let testActivePatternSignatureWithSubStringName (expr: Quotations.Expr) = + match expr with + | Value (o, t) -> (o, t) + | _ -> failwith "no value match" + |> ignore + + match expr with + | DefaultValue t -> t + | _ -> failwith "no value match" + |> ignore + + match expr with + | ValueWithName t -> t + | _ -> failwith "no value match" \ No newline at end of file diff --git a/test/FsAutoComplete.Tests.Lsp/UnsedDeclarationsTests.fs b/test/FsAutoComplete.Tests.Lsp/UnsedDeclarationsTests.fs index a76e1de5c..7f9d5c383 100644 --- a/test/FsAutoComplete.Tests.Lsp/UnsedDeclarationsTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/UnsedDeclarationsTests.fs @@ -1,4 +1,5 @@ module FsAutoComplete.Tests.UnusedDeclarationsTests + open Expecto open FsAutoComplete open Helpers @@ -13,152 +14,166 @@ open Helpers.Expecto.ShadowedTimeouts type private Expected = | Used | Unused + type private Doc = | Untitled - | Existing of path:string -let private checkUsageAt - server - doc - sourceWithoutCursor - cursor - expected - = async { + | Existing of path: string + +let private checkUsageAt server doc sourceWithoutCursor cursor expected = + async { let source = sourceWithoutCursor |> Text.trimTripleQuotation + let! (doc, diags) = let getDoc = match doc with | Untitled -> Server.createUntitledDocument | Existing path -> Server.openDocumentWithText path + server |> getDoc source - use doc = doc + + use _doc = doc let diagsAtCursor = - diags - |> Array.filter (fun d -> d.Range |> Range.containsStrictly cursor) + diags |> Array.filter (fun d -> d.Range |> Range.containsStrictly cursor) let isUnused (diag: Diagnostic) = diag.Source = Some "FSAC" - && - diag.Code = Some "FSAC0003" - && - diag.Message = "This value is unused" - && - diag.Tags |> Option.map (Array.contains DiagnosticTag.Unnecessary) |> Option.defaultValue false + && diag.Code = Some "FSAC0003" + && diag.Message = "This value is unused" + && diag.Tags + |> Option.map (Array.contains DiagnosticTag.Unnecessary) + |> Option.defaultValue false let diag = diagsAtCursor |> Array.filter isUnused + match expected with - | Unused -> - Expect.hasLength diag 1 "There should be exactly one unused value diagnostic at cursor position" - | Used -> - Expect.hasLength diag 0 "There should be no unused value diagnostic at cursor position" + | Unused -> Expect.hasLength diag 1 "There should be exactly one unused value diagnostic at cursor position" + | Used -> Expect.hasLength diag 0 "There should be no unused value diagnostic at cursor position" } -let private checkUsage - server - doc - sourceWithCursor - expected - = async { - let (cursor, source) = - sourceWithCursor - |> Text.trimTripleQuotation - |> Cursor.assertExtractPosition +let private checkUsage server doc sourceWithCursor expected = + async { + let cursor, source = + sourceWithCursor |> Text.trimTripleQuotation |> Cursor.assertExtractPosition + do! checkUsageAt server doc source cursor expected } let private scriptTests state = - let config = { - defaultConfigDto with - UnusedDeclarationsAnalyzer = Some true - } - serverTestList "script" state config None (fun server -> [ - testCaseAsync "unused variable" <| - checkUsage server Untitled + let config = + { defaultConfigDto with + UnusedDeclarationsAnalyzer = Some true } + + serverTestList "script" state config None (fun server -> + [ testCaseAsync "unused variable" + <| checkUsage + server + Untitled """ let $0alpha = 42 """ Unused - testCaseAsync "used variable" <| - checkUsage server Untitled + testCaseAsync "used variable" + <| checkUsage + server + Untitled """ let $0alpha = 42 let _ = alpha """ Used - testCaseAsync "unused tuple element variable" <| - checkUsage server Untitled + testCaseAsync "unused tuple element variable" + <| checkUsage + server + Untitled """ let (alpha, $0beta) = (42, 42) let _ = alpha """ Unused - testCaseAsync "used tuple element variable" <| - checkUsage server Untitled + testCaseAsync "used tuple element variable" + <| checkUsage + server + Untitled """ let ($0alpha, beta) = (42, 42) let _ = alpha """ Used - testCaseAsync "unused this" <| - checkUsage server Untitled + testCaseAsync "unused this" + <| checkUsage + server + Untitled """ type T() = member $0this.F _ = () """ Unused - testCaseAsync "used this" <| - checkUsage server Untitled + testCaseAsync "used this" + <| checkUsage + server + Untitled """ type T() = member $0this.F _ = this.GetType() |> ignore """ Used - testCaseAsync "unused function variable" <| - checkUsage server Untitled + testCaseAsync "unused function variable" + <| checkUsage + server + Untitled """ let $0f _ = () """ Unused - testCaseAsync "used function variable" <| - checkUsage server Untitled + testCaseAsync "used function variable" + <| checkUsage + server + Untitled """ let $0f _ = () f () """ - Used - ]) + Used ]) let private projectTests state = let path = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "UnusedDeclarations") let file = Existing "Library.fs" - let config = { - defaultConfigDto with - UnusedDeclarationsAnalyzer = Some true - } - serverTestList "project" state config (Some path) (fun server -> [ - // Note: public variables aren't considered unused (-> can be used somewhere else) + let config = + { defaultConfigDto with + UnusedDeclarationsAnalyzer = Some true } + + serverTestList "project" state config (Some path) (fun server -> + [ + // Note: public variables aren't considered unused (-> can be used somewhere else) - testCaseAsync "unused private variable" <| - checkUsage server file + testCaseAsync "unused private variable" + <| checkUsage + server + file """ module Root let private $0alpha = 42 """ Unused - testCaseAsync "unused public variable" <| - checkUsage server file + testCaseAsync "unused public variable" + <| checkUsage + server + file """ module Root let $0alpha = 42 """ Used - testCaseAsync "used private variable" <| - checkUsage server file + testCaseAsync "used private variable" + <| checkUsage + server + file """ // module Root @@ -167,10 +182,13 @@ let private projectTests state = let _ = alpha """ Used - testCaseAsync "unused private tuple element variable" <| + testCaseAsync "unused private tuple element variable" + <| // Note: `let private (alpha, beta) =` is incorrect! // must be: `let (private alpha, private beta) =` - checkUsage server file + checkUsage + server + file """ module Root @@ -178,8 +196,10 @@ let private projectTests state = let _ = alpha """ Unused - testCaseAsync "used private tuple element variable" <| - checkUsage server file + testCaseAsync "used private tuple element variable" + <| checkUsage + server + file """ module Root @@ -187,8 +207,10 @@ let private projectTests state = let _ = alpha """ Used - testCaseAsync "unused public tuple element variable" <| - checkUsage server file + testCaseAsync "unused public tuple element variable" + <| checkUsage + server + file """ module Root @@ -196,8 +218,10 @@ let private projectTests state = let _ = alpha """ Used - testCaseAsync "used public tuple element variable" <| - checkUsage server file + testCaseAsync "used public tuple element variable" + <| checkUsage + server + file """ module Root @@ -206,8 +230,10 @@ let private projectTests state = """ Used - testCaseAsync "unused this" <| - checkUsage server file + testCaseAsync "unused this" + <| checkUsage + server + file """ module Root @@ -215,8 +241,10 @@ let private projectTests state = member $0this.F _ = () """ Unused - testCaseAsync "used this" <| - checkUsage server file + testCaseAsync "used this" + <| checkUsage + server + file """ module Root @@ -226,16 +254,20 @@ let private projectTests state = Used - testCaseAsync "unused private function variable" <| - checkUsage server file + testCaseAsync "unused private function variable" + <| checkUsage + server + file """ module Root let private $0f _ = () """ Unused - testCaseAsync "used private function variable" <| - checkUsage server file + testCaseAsync "used private function variable" + <| checkUsage + server + file """ module Root @@ -243,8 +275,10 @@ let private projectTests state = f () """ Used - testCaseAsync "unused public function variable" <| - checkUsage server file + testCaseAsync "unused public function variable" + <| checkUsage + server + file """ module Root @@ -252,11 +286,14 @@ let private projectTests state = """ Used - // https://github.com/fsharp/FsAutoComplete/issues/832 - testList "issue #832" [ - // `$P`: used (or public -> not marked unused) - // `$U`: unused - let source = """ + // https://github.com/fsharp/FsAutoComplete/issues/832 + testList + "issue #832" + [ + // `$P`: used (or public -> not marked unused) + // `$U`: unused + let source = + """ module External = let $Pa = 123 @@ -276,25 +313,19 @@ module private Internal = let private $Uy _ = () """ - let (source, cursors) = - source - |> Text.trimTripleQuotation - |> Cursors.extractWith [| "$P"; "$U" |] - - for (marker, pos) in cursors do - let expected = if marker = "$P" then Used else Unused - let title = $"%A{expected} at %s{pos.DebuggerDisplay}" - testCaseAsync title <| - checkUsageAt server file - source pos - expected - ] - ]) + let (source, cursors) = + source |> Text.trimTripleQuotation |> Cursors.extractWith [| "$P"; "$U" |] + + for (marker, pos) in cursors do + let expected = if marker = "$P" then Used else Unused + let title = $"%A{expected} at %s{pos.DebuggerDisplay}" + testCaseAsync title <| checkUsageAt server file source pos expected ] ]) let tests state = - testList ("Unused Declarations") [ - // Note: difference between Script & Project: - // Public in Script can be unused, public in Project cannot - scriptTests state - projectTests state - ] + testList + ("Unused Declarations") + [ + // Note: difference between Script & Project: + // Public in Script can be unused, public in Project cannot + scriptTests state + projectTests state ] diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs b/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs index e861e8746..00fa8725f 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs @@ -1,6 +1,6 @@ module Utils.CursorbasedTests + open Expecto -open Expecto.Diff open Ionide.LanguageServerProtocol.Types open FsToolkit.ErrorHandling open Utils.Utils @@ -14,11 +14,10 @@ open Ionide.ProjInfo.Logging /// * `check`: Check to use inside a `testCaseAsync`. Not a Test itself! /// * `test`: Returns Expecto Test. Usually combines multiple tests (like: test all positions). module CodeFix = - let private logger = LogProvider.getLoggerByName "CursorbasedTests.CodeFix" + let private _logger = LogProvider.getLoggerByName "CursorbasedTests.CodeFix" let private diagnosticsIn (range: Range) (diags: Diagnostic[]) = - diags - |> Array.filter (fun diag -> range |> Range.overlapsStrictly diag.Range) + diags |> Array.filter (fun diag -> range |> Range.overlapsStrictly diag.Range) /// Note: Return should be just ONE `CodeAction` (for Applicable) or ZERO `CodeAction` (for Not Applicable). /// But actual return type is an array of `CodeAction`s: @@ -26,6 +25,7 @@ module CodeFix = /// * Returning `CodeAction option` would mean different filters for `check` (exactly one fix) and `checkNotApplicable` (exactly zero fix). /// Both error with multiple matching fixes! type ChooseFix = CodeAction[] -> CodeAction[] + type ExpectedResult = | NotApplicable | Applicable @@ -33,25 +33,27 @@ module CodeFix = let checkFixAt (doc: Document, diagnostics: Diagnostic[]) + (editsFrom: VersionedTextDocumentIdentifier) (beforeWithoutCursor: string, cursorRange: Range) (validateDiagnostics: Diagnostic[] -> unit) (chooseFix: ChooseFix) (expected: ExpectedResult) - = async { + = + async { // filter to only diags matching the cursor range let diags = diagnostics |> diagnosticsIn cursorRange validateDiagnostics diags // get code fixes from server let! res = doc |> Document.codeActionAt diags cursorRange + let allCodeActions = match res, expected with | None, (Applicable | After _) -> - // error here instead of later to return error noting it was `None` instead of empty CodeAction array - Expect.isSome res "No CodeAction returned (`None`)" - failwith "unreachable" - | None, NotApplicable -> - [||] - | Some (Helpers.CodeActions actions), _ -> actions + // error here instead of later to return error noting it was `None` instead of empty CodeAction array + Expect.isSome res "No CodeAction returned (`None`)" + failwith "unreachable" + | None, NotApplicable -> [||] + | Some(Helpers.CodeActions actions), _ -> actions | Some _, _ -> failwith "Expected some code actions from the server" @@ -71,42 +73,41 @@ module CodeFix = match expected with | NotApplicable -> // Expect.isEmpty codeActions "There should be no applicable code action" // doesn't show `actual` when not empty - if not (codeActions |> Array.isEmpty) then + if not (codeActions |> Array.isEmpty) then failtestf "There should be no applicable code action, but was %A" codeActions - | Applicable -> - codeActions - |> getCodeAction - |> ignore - //ENHANCEMENT?: apply edits to check valid? + | Applicable -> codeActions |> getCodeAction |> ignore + //ENHANCEMENT?: apply edits to check valid? | After expected -> - let codeAction = codeActions |> getCodeAction + let codeAction = codeActions |> getCodeAction + + /// Error message is appended by selected `codeAction` + let inline failCodeFixTest (msg: string) = + let msg = + if + System.String.IsNullOrWhiteSpace msg + || System.Char.IsPunctuation(msg, msg.Length - 1) + then + msg + else + msg + "." - /// Error message is appended by selected `codeAction` - let inline failCodeFixTest (msg: string) = - let msg = - if System.String.IsNullOrWhiteSpace msg || System.Char.IsPunctuation(msg, msg.Length-1) then - msg - else - msg + "." - failtest $"{msg} CodeAction was: %A{codeAction}" - - // only text edits supported - if codeAction.Command |> Option.isSome then - failCodeFixTest "Code action contains commands. Commands aren't supported in this test!" - - let edits = - codeAction.Edit - |> Option.defaultWith (fun _ -> failCodeFixTest "Code action doesn't contain any edits") - |> WorkspaceEdit.tryExtractTextEditsInSingleFile doc.VersionedTextDocumentIdentifier - |> Result.valueOr failCodeFixTest - - // apply fix - let actual = - beforeWithoutCursor - |> TextEdits.apply edits - |> Result.valueOr failCodeFixTest - - Expecto.Diff.equals actual expected "Incorrect text after applying the chosen code action" + failtest $"{msg} CodeAction was: %A{codeAction}" + + // only text edits supported + if codeAction.Command |> Option.isSome then + failCodeFixTest "Code action contains commands. Commands aren't supported in this test!" + + let edits = + codeAction.Edit + |> Option.defaultWith (fun _ -> failCodeFixTest "Code action doesn't contain any edits") + |> WorkspaceEdit.tryExtractTextEditsInSingleFile editsFrom + |> Result.valueOr failCodeFixTest + + // apply fix + let actual = + beforeWithoutCursor |> TextEdits.apply edits |> Result.valueOr failCodeFixTest + + Expecto.Diff.equals actual expected "Incorrect text after applying the chosen code action" } let private checkFix @@ -115,16 +116,22 @@ module CodeFix = (validateDiagnostics: Diagnostic[] -> unit) (chooseFix: ChooseFix) (expected: unit -> ExpectedResult) - = async { + = + async { let (range, text) = - beforeWithCursor - |> Text.trimTripleQuotation - |> Cursor.assertExtractRange + beforeWithCursor |> Text.trimTripleQuotation |> Cursor.assertExtractRange // load text file let! (doc, diags) = server |> Server.createUntitledDocument text use doc = doc // ensure doc gets closed (disposed) after test - do! checkFixAt (doc, diags) (text, range) validateDiagnostics chooseFix (expected()) + do! + checkFixAt + (doc, diags) + doc.VersionedTextDocumentIdentifier + (text, range) + validateDiagnostics + chooseFix + (expected ()) } /// Checks a CodeFix (CodeAction) for validity. @@ -158,50 +165,18 @@ module CodeFix = /// Linebreaks from edits in selected CodeFix are all transformed to just `\n` /// -> CodeFix can use `\r` and `\r\n` /// If you want to validate Line Endings of CodeFix, add a validation step to your `chooseFix` - let check - server - beforeWithCursor - validateDiagnostics - chooseFix - expected - = - checkFix - server - beforeWithCursor - validateDiagnostics - chooseFix - (fun () -> After (expected |> Text.trimTripleQuotation)) + let check server beforeWithCursor validateDiagnostics chooseFix expected = + checkFix server beforeWithCursor validateDiagnostics chooseFix (fun () -> + After(expected |> Text.trimTripleQuotation)) /// Note: Doesn't apply Fix! Just checks its existence! - let checkApplicable - server - beforeWithCursor - validateDiagnostics - chooseFix - = - checkFix - server - beforeWithCursor - validateDiagnostics - chooseFix - (fun () -> Applicable) - - let checkNotApplicable - server - beforeWithCursor - validateDiagnostics - chooseFix - = - checkFix - server - beforeWithCursor - validateDiagnostics - chooseFix - (fun () -> NotApplicable) - - let matching cond (fixes: CodeAction array) = - fixes - |> Array.filter cond + let checkApplicable server beforeWithCursor validateDiagnostics chooseFix = + checkFix server beforeWithCursor validateDiagnostics chooseFix (fun () -> Applicable) + + let checkNotApplicable server beforeWithCursor validateDiagnostics chooseFix = + checkFix server beforeWithCursor validateDiagnostics chooseFix (fun () -> NotApplicable) + + let matching cond (fixes: CodeAction array) = fixes |> Array.filter cond let withTitle title = matching (fun f -> f.Title = title) let ofKind kind = matching (fun f -> f.Kind = Some kind) @@ -225,18 +200,29 @@ module CodeFix = (expected: ExpectedResult) = Expect.isNonEmpty cursorRanges "No Range(s) specified" - ServerTests.documentTestList name server (Server.createUntitledDocument beforeWithoutCursor) (fun doc -> [ - for (i, range) in cursorRanges |> Seq.indexed do - let pos = - if range |> Range.isPosition then - range.Start.DebuggerDisplay - else - $"{range.Start.DebuggerDisplay}..{range.End.DebuggerDisplay}" - testCaseAsync $"Cursor {i} at {pos}" (async { - let! (doc, diags) = doc - do! checkFixAt (doc, diags) (beforeWithoutCursor, range) validateDiagnostics chooseFix expected - }) - ]) + + ServerTests.documentTestList name server (Server.createUntitledDocument beforeWithoutCursor) (fun doc -> + [ for (i, range) in cursorRanges |> Seq.indexed do + let pos = + if range |> Range.isPosition then + range.Start.DebuggerDisplay + else + $"{range.Start.DebuggerDisplay}..{range.End.DebuggerDisplay}" + + testCaseAsync + $"Cursor {i} at {pos}" + (async { + let! (doc, diags) = doc + + do! + checkFixAt + (doc, diags) + doc.VersionedTextDocumentIdentifier + (beforeWithoutCursor, range) + validateDiagnostics + chooseFix + expected + }) ]) /// One test for each Cursor. /// @@ -250,51 +236,18 @@ module CodeFix = (chooseFix: ChooseFix) (expected: unit -> ExpectedResult) = - let (beforeWithoutCursor, poss) = beforeWithCursors |> Text.trimTripleQuotation |> Cursors.extract + let (beforeWithoutCursor, poss) = + beforeWithCursors |> Text.trimTripleQuotation |> Cursors.extract + let ranges = poss |> List.map (fun p -> { Start = p; End = p }) - checkFixAll name server beforeWithoutCursor ranges validateDiagnostics chooseFix (expected()) - - let testAllPositions - name - server - beforeWithCursors - validateDiagnostics - chooseFix - expected - = - Test.checkAllPositions - name - server - beforeWithCursors - validateDiagnostics - chooseFix - (fun () -> After (expected |> Text.trimTripleQuotation)) - - let testApplicableAllPositions - name - server - beforeWithCursors - validateDiagnostics - chooseFix - = - Test.checkAllPositions - name - server - beforeWithCursors - validateDiagnostics - chooseFix - (fun () -> Applicable) - let testNotApplicableAllPositions - name - server - beforeWithCursors - validateDiagnostics - chooseFix - = - Test.checkAllPositions - name - server - beforeWithCursors - validateDiagnostics - chooseFix - (fun () -> NotApplicable) + checkFixAll name server beforeWithoutCursor ranges validateDiagnostics chooseFix (expected ()) + + let testAllPositions name server beforeWithCursors validateDiagnostics chooseFix expected = + Test.checkAllPositions name server beforeWithCursors validateDiagnostics chooseFix (fun () -> + After(expected |> Text.trimTripleQuotation)) + + let testApplicableAllPositions name server beforeWithCursors validateDiagnostics chooseFix = + Test.checkAllPositions name server beforeWithCursors validateDiagnostics chooseFix (fun () -> Applicable) + + let testNotApplicableAllPositions name server beforeWithCursors validateDiagnostics chooseFix = + Test.checkAllPositions name server beforeWithCursors validateDiagnostics chooseFix (fun () -> NotApplicable) diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fsi b/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fsi index 901e1e9ad..0812f801e 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fsi +++ b/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fsi @@ -1,13 +1,8 @@ module Utils.CursorbasedTests open Expecto -open Expecto.Diff open Ionide.LanguageServerProtocol.Types -open FsToolkit.ErrorHandling -open Utils.Utils open Utils.Server -open Utils.TextEdit -open Ionide.ProjInfo.Logging /// Checks for CodeFixes, CodeActions /// @@ -15,138 +10,139 @@ open Ionide.ProjInfo.Logging /// * `check`: Check to use inside a `testCaseAsync`. Not a Test itself! /// * `test`: Returns Expecto Test. Usually combines multiple tests (like: test all positions). module CodeFix = - /// Note: Return should be just ONE `CodeAction` (for Applicable) or ZERO `CodeAction` (for Not Applicable). - /// But actual return type is an array of `CodeAction`s: - /// * Easier to successive filter CodeActions down with simple pipe and `Array.filter` - /// * Returning `CodeAction option` would mean different filters for `check` (exactly one fix) and `checkNotApplicable` (exactly zero fix). - /// Both error with multiple matching fixes! - type ChooseFix = CodeAction[] -> CodeAction[] + /// Note: Return should be just ONE `CodeAction` (for Applicable) or ZERO `CodeAction` (for Not Applicable). + /// But actual return type is an array of `CodeAction`s: + /// * Easier to successive filter CodeActions down with simple pipe and `Array.filter` + /// * Returning `CodeAction option` would mean different filters for `check` (exactly one fix) and `checkNotApplicable` (exactly zero fix). + /// Both error with multiple matching fixes! + type ChooseFix = CodeAction[] -> CodeAction[] - type ExpectedResult = - | NotApplicable - | Applicable - | After of string + type ExpectedResult = + | NotApplicable + | Applicable + | After of string - val checkFixAt: - doc: Document * diagnostics: Diagnostic[] -> - beforeWithoutCursor: string * cursorRange: Range -> - validateDiagnostics: (Diagnostic[] -> unit) -> - chooseFix: ChooseFix -> - expected: ExpectedResult -> - Async - - /// Checks a CodeFix (CodeAction) for validity. - /// - /// * Extracts cursor position (`$0`) or range (between two `$0`) from `beforeWithCursor` - /// * Opens untitled Doc with source `beforeWithCursor` (with cursor removed) - /// * Note: untitled Document acts as Script file! - /// * Note: untitled Documents doesn't exist on disk! - /// * Waits for Diagnostics in that doc - /// * Filters Diags down to diags matching cursor position/range - /// * Then validates diags with `validateDiagnostics` - /// * Note: Validates filtered diags (-> only diags at cursor pos); not all diags in doc! - /// * Gets CodeFixes (CodeActions) from LSP server (`textDocument/codeAction`) for cursor range - /// * Request includes filtered diags - /// * Selects CodeFix from returned CodeFixes with `chooseFix` - /// * Note: `chooseFix` should return a single CodeFix. No CodeFix or multiple CodeFixes count as Failure! - /// * Use `checkNotApplicable` when there shouldn't be a CodeFix - /// * Note: Though `chooseFix` should return one CodeFix, the function actually returns an array of CodeFixes. - /// Reasons: - /// * Easier to filter down CodeFixes (`CodeFix.ofKind "..." >> CodeFix.withTitle "..."`) - /// * Better error messages: Can differentiate between no CodeFixes and too many CodeFixes - /// * Validates selected CodeFix: - /// * Applies selected CodeFix to source (`beforeWithCursor` with cursor removed) - /// * Compares result with `expected` - /// - /// Note: - /// `beforeWithCursor` as well as `expected` get trimmed with `Text.trimTripleQuotation`: Leading empty line and indentation gets removed. - /// - /// Note: - /// `beforeWithCursor` and `expected` MUST use `\n` for linebreaks -- using `\r` (either alone or as `\r\n`) results in test failure! - /// Linebreaks from edits in selected CodeFix are all transformed to just `\n` - /// -> CodeFix can use `\r` and `\r\n` - /// If you want to validate Line Endings of CodeFix, add a validation step to your `chooseFix` - val check: - server: CachedServer -> - beforeWithCursor: string -> - validateDiagnostics: (Diagnostic array -> unit) -> + val checkFixAt: + doc: Document * diagnostics: Diagnostic[] -> + editsFrom: VersionedTextDocumentIdentifier -> + beforeWithoutCursor: string * cursorRange: Range -> + validateDiagnostics: (Diagnostic[] -> unit) -> chooseFix: ChooseFix -> - expected: string -> - Async + expected: ExpectedResult -> + Async - /// Note: Doesn't apply Fix! Just checks its existence! - val checkApplicable: - server: CachedServer -> - beforeWithCursor: string -> - validateDiagnostics: (Diagnostic array -> unit) -> - chooseFix: ChooseFix -> - Async + /// Checks a CodeFix (CodeAction) for validity. + /// + /// * Extracts cursor position (`$0`) or range (between two `$0`) from `beforeWithCursor` + /// * Opens untitled Doc with source `beforeWithCursor` (with cursor removed) + /// * Note: untitled Document acts as Script file! + /// * Note: untitled Documents doesn't exist on disk! + /// * Waits for Diagnostics in that doc + /// * Filters Diags down to diags matching cursor position/range + /// * Then validates diags with `validateDiagnostics` + /// * Note: Validates filtered diags (-> only diags at cursor pos); not all diags in doc! + /// * Gets CodeFixes (CodeActions) from LSP server (`textDocument/codeAction`) for cursor range + /// * Request includes filtered diags + /// * Selects CodeFix from returned CodeFixes with `chooseFix` + /// * Note: `chooseFix` should return a single CodeFix. No CodeFix or multiple CodeFixes count as Failure! + /// * Use `checkNotApplicable` when there shouldn't be a CodeFix + /// * Note: Though `chooseFix` should return one CodeFix, the function actually returns an array of CodeFixes. + /// Reasons: + /// * Easier to filter down CodeFixes (`CodeFix.ofKind "..." >> CodeFix.withTitle "..."`) + /// * Better error messages: Can differentiate between no CodeFixes and too many CodeFixes + /// * Validates selected CodeFix: + /// * Applies selected CodeFix to source (`beforeWithCursor` with cursor removed) + /// * Compares result with `expected` + /// + /// Note: + /// `beforeWithCursor` as well as `expected` get trimmed with `Text.trimTripleQuotation`: Leading empty line and indentation gets removed. + /// + /// Note: + /// `beforeWithCursor` and `expected` MUST use `\n` for linebreaks -- using `\r` (either alone or as `\r\n`) results in test failure! + /// Linebreaks from edits in selected CodeFix are all transformed to just `\n` + /// -> CodeFix can use `\r` and `\r\n` + /// If you want to validate Line Endings of CodeFix, add a validation step to your `chooseFix` + val check: + server: CachedServer -> + beforeWithCursor: string -> + validateDiagnostics: (Diagnostic array -> unit) -> + chooseFix: ChooseFix -> + expected: string -> + Async - val checkNotApplicable: - server: CachedServer -> - beforeWithCursor: string -> - validateDiagnostics: (Diagnostic array -> unit) -> - chooseFix: ChooseFix -> - Async + /// Note: Doesn't apply Fix! Just checks its existence! + val checkApplicable: + server: CachedServer -> + beforeWithCursor: string -> + validateDiagnostics: (Diagnostic array -> unit) -> + chooseFix: ChooseFix -> + Async - val matching: cond: (CodeAction -> bool) -> fixes: CodeAction array -> CodeAction array - val withTitle: title: string -> (CodeAction array -> CodeAction array) - val ofKind: kind: string -> (CodeAction array -> CodeAction array) + val checkNotApplicable: + server: CachedServer -> + beforeWithCursor: string -> + validateDiagnostics: (Diagnostic array -> unit) -> + chooseFix: ChooseFix -> + Async - /// Bundled tests in Expecto test - module private Test = - /// One `testCaseAsync` for each cursorRange. - /// All test cases use same document (`ServerTests.documentTestList`) with source `beforeWithoutCursor`. - /// - /// Test names: - /// * `name` is name of outer test list. - /// * Each test case: `Cursor {i} at {pos or range}` - /// - /// Note: Sharing a common `Document` is just barely faster than using a new `Document` for each test (at least for simple source in `beforeWithoutCursor`). - val checkFixAll: - name: string -> - server: CachedServer -> - beforeWithoutCursor: string -> - cursorRanges: Range seq -> - validateDiagnostics: (Diagnostic[] -> unit) -> - chooseFix: ChooseFix -> - expected: ExpectedResult -> - Test + val matching: cond: (CodeAction -> bool) -> fixes: CodeAction array -> CodeAction array + val withTitle: title: string -> (CodeAction array -> CodeAction array) + val ofKind: kind: string -> (CodeAction array -> CodeAction array) - /// One test for each Cursor. - /// - /// Note: Tests single positions -> each `$0` gets checked. - /// -> Every test is for single-position range (`Start=End`)! - val checkAllPositions: - name: string -> - server: CachedServer -> - beforeWithCursors: string -> - validateDiagnostics: (Diagnostic[] -> unit) -> - chooseFix: ChooseFix -> - expected: (unit -> ExpectedResult) -> - Test + /// Bundled tests in Expecto test + module private Test = + /// One `testCaseAsync` for each cursorRange. + /// All test cases use same document (`ServerTests.documentTestList`) with source `beforeWithoutCursor`. + /// + /// Test names: + /// * `name` is name of outer test list. + /// * Each test case: `Cursor {i} at {pos or range}` + /// + /// Note: Sharing a common `Document` is just barely faster than using a new `Document` for each test (at least for simple source in `beforeWithoutCursor`). + val checkFixAll: + name: string -> + server: CachedServer -> + beforeWithoutCursor: string -> + cursorRanges: Range seq -> + validateDiagnostics: (Diagnostic[] -> unit) -> + chooseFix: ChooseFix -> + expected: ExpectedResult -> + Test - val testAllPositions: - name: string -> - server: CachedServer -> - beforeWithCursors: string -> - validateDiagnostics: (Diagnostic array -> unit) -> - chooseFix: ChooseFix -> - expected: string -> - Test + /// One test for each Cursor. + /// + /// Note: Tests single positions -> each `$0` gets checked. + /// -> Every test is for single-position range (`Start=End`)! + val checkAllPositions: + name: string -> + server: CachedServer -> + beforeWithCursors: string -> + validateDiagnostics: (Diagnostic[] -> unit) -> + chooseFix: ChooseFix -> + expected: (unit -> ExpectedResult) -> + Test - val testApplicableAllPositions: - name: string -> - server: CachedServer -> - beforeWithCursors: string -> - validateDiagnostics: (Diagnostic array -> unit) -> - chooseFix: ChooseFix -> - Test + val testAllPositions: + name: string -> + server: CachedServer -> + beforeWithCursors: string -> + validateDiagnostics: (Diagnostic array -> unit) -> + chooseFix: ChooseFix -> + expected: string -> + Test - val testNotApplicableAllPositions: - name: string -> - server: CachedServer -> - beforeWithCursors: string -> - validateDiagnostics: (Diagnostic array -> unit) -> - chooseFix: ChooseFix -> - Test + val testApplicableAllPositions: + name: string -> + server: CachedServer -> + beforeWithCursors: string -> + validateDiagnostics: (Diagnostic array -> unit) -> + chooseFix: ChooseFix -> + Test + + val testNotApplicableAllPositions: + name: string -> + server: CachedServer -> + beforeWithCursors: string -> + validateDiagnostics: (Diagnostic array -> unit) -> + chooseFix: ChooseFix -> + Test diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/Server.Tests.fs b/test/FsAutoComplete.Tests.Lsp/Utils/Server.Tests.fs index 6281501f6..9fcd9fc98 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/Server.Tests.fs +++ b/test/FsAutoComplete.Tests.Lsp/Utils/Server.Tests.fs @@ -1,4 +1,5 @@ module Utils.Tests.Server + open System open Expecto open Helpers @@ -13,470 +14,645 @@ open Utils.Utils open FsToolkit.ErrorHandling open FSharpx.Control -let tests state = testList (nameof(Server)) [ - testList "no root path" [ - testList "can get diagnostics" [ - let config = - { defaultConfigDto with - UnusedOpensAnalyzer = Some false - UnusedDeclarationsAnalyzer = Some false - SimplifyNameAnalyzer = Some false - } - serverTestList "no analyzers" state config None (fun server -> [ - testCaseAsync "can get nothing wrong" (async { - let! (doc, diags) = server |> Server.createUntitledDocument "" - use doc = doc - Expect.isEmpty diags "There should be no diagnostics" - - for i in 1..5 do - let! diags = doc |> Document.changeTextTo (string i) - Expect.isEmpty diags "There should be no diagnostics" - }) - testCaseAsync "can get single error" (async { - let! (doc, diags) = server |> Server.createUntitledDocument "let foo = notdefined" - use doc = doc - Expect.hasLength diags 1 "There should be 1 error" - Expect.exists diags (fun d -> d.Message.Contains "notdefined") "" - let! diags = doc |> Document.changeTextTo "let bar = doesnotexist" - Expect.hasLength diags 1 "There should be 1 error" - Expect.exists diags (fun d -> d.Message.Contains "doesnotexist") "" - let! diags = doc |> Document.changeTextTo "let baz = nope" - Expect.hasLength diags 1 "There should be 1 error" - Expect.exists diags (fun d -> d.Message.Contains "nope") "" - }) - testCaseAsync "can get multiple errors" (async { - let source = "let foo = {0}\nlet bar = {1}\nlet baz = {2}" - let names = [|"notdefined"; "doesnotexist"; "nope"|] - let fnames i = names |> Array.map (fun n -> sprintf "%s%i" n i) - let fsource i = String.Format(source, fnames i |> Seq.cast |> Seq.toArray) - - let! (doc, diags) = server |> Server.createUntitledDocument (fsource 0) - use doc = doc - Expect.hasLength diags (names.Length) "" - for name in fnames 0 do - Expect.exists diags (fun d -> d.Message.Contains name) "" - - for i in 1..2 do - let! diags = doc |> Document.changeTextTo (fsource i) - Expect.hasLength diags (names.Length) "" - for name in fnames i do - Expect.exists diags (fun d -> d.Message.Contains name) "" - }) - ]) - - let config = - { defaultConfigDto with - UnusedOpensAnalyzer = Some false - UnusedDeclarationsAnalyzer = Some true - SimplifyNameAnalyzer = Some false - } - serverTestList "just unused decl analyzer" state config None (fun server -> [ - testCaseAsync "can get nothing wrong" <| (async { - let! (doc, diags) = server |> Server.createUntitledDocument "" - use doc = doc - Expect.isEmpty diags "There should be no diagnostics" - - for i in 1..5 do - let! diags = doc |> Document.changeTextTo (string i) - Expect.isEmpty diags "There should be no diagnostics" - }) - testCaseAsync "can get diags for single line" (async { - let! (doc, diags) = server |> Server.createUntitledDocument "let foo = notdefined" - use doc = doc - Expect.hasLength diags 2 "" - Expect.exists diags (fun d -> d.Message.Contains "notdefined") "" - Expect.exists diags (fun d -> d.Message = "This value is unused") "" - let! diags = doc |> Document.changeTextTo "let bar = doesnotexist" - Expect.hasLength diags 2 "" - Expect.exists diags (fun d -> d.Message.Contains "doesnotexist") "" - Expect.exists diags (fun d -> d.Message = "This value is unused") "" - let! diags = doc |> Document.changeTextTo "let baz = nope" - Expect.hasLength diags 2 "" - Expect.exists diags (fun d -> d.Message.Contains "nope") "" - Expect.exists diags (fun d -> d.Message = "This value is unused") "" - }) - testCaseAsync "can get diags for multiple lines" (async { - let nVars = 3 - let values i = Array.init nVars (sprintf "someValue%i%i" i) - let source i = - values i - |> Seq.mapi (sprintf "let var%i = %s") - |> String.concat "\n" - - let! (doc, diags) = server |> Server.createUntitledDocument (source 0) - use doc = doc - Expect.hasLength diags (nVars * 2) "" - values 0 - |> Array.iteri (fun i name -> - Expect.exists diags (fun d -> d.Message.Contains name) $"No diags with name {name}" - Expect.exists diags (fun d -> d.Message = "This value is unused" && d.Range.Start.Line = i) $"No unused value error in line {i}" - ) - - for i in 1..2 do - let! diags = doc |> Document.changeTextTo (source i) - Expect.hasLength diags (nVars * 2) "" - values i - |> Array.iteri (fun i name -> - Expect.exists diags (fun d -> d.Message.Contains name) $"No diags with name {name}" - Expect.exists diags (fun d -> d.Message = "This value is unused" && d.Range.Start.Line = i) $"No unused value error in line {i}" - ) - }) - ]) - - let config = - { defaultConfigDto with - UnusedOpensAnalyzer = Some true - UnusedDeclarationsAnalyzer = Some true - SimplifyNameAnalyzer = Some true - } - serverTestList "three analyzers" state config None (fun server -> [ - testCaseAsync "can get nothing wrong" (async { - let! (doc, diags) = server |> Server.createUntitledDocument "" - use doc = doc - Expect.isEmpty diags "There should be no diagnostics" - - for i in 1..5 do - let! diags = doc |> Document.changeTextTo (string i) - Expect.isEmpty diags "There should be no diagnostics" - }) - testCaseAsync "can get all diags" (async { - let source = "open System\nlet foo = bar\nSystem.String.Empty |> ignore" - let! (doc, diags) = server |> Server.createUntitledDocument source - use doc = doc - - Expect.hasLength diags 4 "" - Expect.exists diags (fun d -> d.Message = "Unused open statement" && d.Range.Start.Line = 0) "" - Expect.exists diags (fun d -> d.Message = "This value is unused" && d.Range.Start.Line = 1) "" - Expect.exists diags (fun d -> d.Message.Contains "bar" && d.Range.Start.Line = 1) "" - Expect.exists diags (fun d -> d.Message = "This qualifier is redundant" && d.Range.Start.Line = 2) "" - - - let source = "open System.Collections\nlet baz = foo\nSystem.Collections.Generic.List() |> ignore" - let! diags = doc |> Document.changeTextTo source - - Expect.hasLength diags 4 "" - Expect.exists diags (fun d -> d.Message = "Unused open statement" && d.Range.Start.Line = 0) "" - Expect.exists diags (fun d -> d.Message = "This value is unused" && d.Range.Start.Line = 1) "" - Expect.exists diags (fun d -> d.Message.Contains "foo" && d.Range.Start.Line = 1) "" - Expect.exists diags (fun d -> d.Message = "This qualifier is redundant" && d.Range.Start.Line = 2) "" - - - let source = "open System.Diagnostics\nlet bar = baz\nSystem.Diagnostics.Debugger.IsAttached" - let! diags = doc |> Document.changeTextTo source - - Expect.hasLength diags 4 "" - Expect.exists diags (fun d -> d.Message = "Unused open statement" && d.Range.Start.Line = 0) "" - Expect.exists diags (fun d -> d.Message = "This value is unused" && d.Range.Start.Line = 1) "" - Expect.exists diags (fun d -> d.Message.Contains "baz" && d.Range.Start.Line = 1) "" - Expect.exists diags (fun d -> d.Message = "This qualifier is redundant" && d.Range.Start.Line = 2) "" - }) - ]) - ] - - testList "untitled document" [ - serverTestList "untitled counter in server for createUntitledDocument" state defaultConfigDto None (fun server -> [ - testCaseAsync "creating document increases untitled counter" (async { - let! actualServer = server - let preCounter = actualServer.UntitledCounter - let! (doc, _) = server |> Server.createUntitledDocument "" - use doc = doc - let postCounter = actualServer.UntitledCounter - - Expect.isGreaterThan postCounter preCounter "Untitled Counter should increase" - }) - testCaseAsync "creating multiple documents increases untitled counter" (async { - let getCounter server = server |> Async.map (fun s -> s.UntitledCounter) - - let! preCounter = getCounter server - let mutable preCounter = preCounter - for i in 1..5 do - let! (doc, _) = server |> Server.createUntitledDocument "" - use doc = doc - let! postCounter = getCounter server - Expect.isGreaterThan postCounter preCounter "Untitled Counter should increase" - preCounter <- postCounter - }) - ]) - - serverTestList "document version" state defaultConfigDto None (fun server -> [ - testCaseAsync "changing document text increases document version" (async { - let! (doc, _) = server |> Server.createUntitledDocument "" - let preVersion = doc.Version - let! _ = doc |> Document.changeTextTo "42" - let postVersion = doc.Version - - Expect.isGreaterThan postVersion preVersion "Document Version should increase" - }) - testCaseAsync "changing document text multiple times should always increase document version" (async { - let! (doc, _) = server |> Server.createUntitledDocument "" - let mutable preVersion = doc.Version - for _ in 1..5 do - let! _ = doc |> Document.changeTextTo "" - let postVersion = doc.Version - Expect.isGreaterThan postVersion preVersion "Document Version should increase" - preVersion <- postVersion - }) - ]) - ] - ] - - testList "with root path" [ - let inTestCases name = - System.IO.Path.Combine(__SOURCE_DIRECTORY__, "..", "TestCases", "ServerTests", name) - |> Some - - let noAnalyzersConfig = - { defaultConfigDto with - UnusedOpensAnalyzer = Some false - UnusedDeclarationsAnalyzer = Some false - SimplifyNameAnalyzer = Some false - } - let allAnalyzersConfig = - { defaultConfigDto with - UnusedOpensAnalyzer = Some true - UnusedDeclarationsAnalyzer = Some true - SimplifyNameAnalyzer = Some true - } - serverTestList "dir with just a script and no analyzers" state noAnalyzersConfig (inTestCases "JustScript") (fun server -> [ - testCaseAsync "can load script file" (async { - let! (doc, diags) = server |> Server.openDocument "Script.fsx" - use doc = doc - - Expect.hasLength diags 1 "Should be one diagnostics" - let diag = diags |> Array.head - Expect.stringContains diag.Message "The value or constructor 'bar' is not defined." "Should be not defined error" - Expect.equal diag.Range.Start.Line 0 "Error should be in line 1" - }) - testCaseAsync "can load script file again" (async { - let! (doc, diags) = server |> Server.openDocument "Script.fsx" - use doc = doc - - Expect.hasLength diags 1 "Should be one diagnostics" - let diag = diags |> Array.head - Expect.stringContains diag.Message "The value or constructor 'bar' is not defined." "Should be not defined error" - Expect.equal diag.Range.Start.Line 0 "Error should be in line 1" - }) - ]) - serverTestList "dir with just a script and all anaylzers" state allAnalyzersConfig (inTestCases "JustScript") (fun server -> [ - testCaseAsync "can load script file" (async { - let! (doc, diags) = server |> Server.openDocument "Script.fsx" - use doc = doc - - Expect.exists diags (fun diag -> diag.Message.Contains "The value or constructor 'bar' is not defined." && diag.Range.Start.Line = 0) "Should be not defined error" - Expect.exists diags (fun diag -> diag.Message.Contains "This value is unused" && diag.Range.Start.Line = 0) "Should be unused value" - }) - testCaseAsync "can load script file again" (async { - let! (doc, diags) = server |> Server.openDocument "Script.fsx" - use doc = doc - - Expect.exists diags (fun diag -> diag.Message.Contains "The value or constructor 'bar' is not defined." && diag.Range.Start.Line = 0) "Should be not defined error" - Expect.exists diags (fun diag -> diag.Message.Contains "This value is unused" && diag.Range.Start.Line = 0) "Should be unused value" - }) - ]) - - testSequenced <| testList "contesting" [ - let projectDir = inTestCases "Project" - serverTestList "dir with project and no analyzers" state noAnalyzersConfig projectDir (fun server -> [ - testCaseAsync "can load file in project" (async { - let! (doc, diags) = server |> Server.openDocument "Other.fs" - use doc = doc - - Expect.hasLength diags 1 "Should be one diagnostics" - let diag = diags |> Array.head - Expect.stringContains diag.Message "The value or constructor 'otherBar' is not defined." "Should be not defined error" - Expect.equal diag.Range.Start.Line 5 "Error should be in line 6" - }) - testCaseAsync "can load file in project again" (async { - let! (doc, diags) = server |> Server.openDocument "Other.fs" - use doc = doc - - Expect.hasLength diags 1 "Should be one diagnostics" - let diag = diags |> Array.head - Expect.stringContains diag.Message "The value or constructor 'otherBar' is not defined." "Should be not defined error" - Expect.equal diag.Range.Start.Line 5 "Error should be in line 6" - }) - testCaseAsync "can load other file in project" (async { - let! (doc, diags) = server |> Server.openDocument "Program.fs" - use doc = doc - - Expect.hasLength diags 1 "Should be one diagnostics" - let diag = diags |> Array.head - Expect.stringContains diag.Message "The value or constructor 'programBar' is not defined." "Should be not defined error" - Expect.equal diag.Range.Start.Line 4 "Error should be in line 5" - }) - ]) - serverTestList "dir with project and all analyzers" state allAnalyzersConfig projectDir (fun server -> [ - testCaseAsync "can load file in project" (async { - let! (doc, diags) = server |> Server.openDocument "Other.fs" - use doc = doc - - Expect.hasLength diags 1 "Should be one diagnostics" - let diag = diags |> Array.head - Expect.stringContains diag.Message "The value or constructor 'otherBar' is not defined." "Should be not defined error" - Expect.equal diag.Range.Start.Line 5 "Error should be in line 6" - }) - testCaseAsync "can load file in project again" (async { - let! (doc, diags) = server |> Server.openDocument "Other.fs" - use doc = doc - - Expect.hasLength diags 1 "Should be one diagnostics" - let diag = diags |> Array.head - Expect.stringContains diag.Message "The value or constructor 'otherBar' is not defined." "Should be not defined error" - Expect.equal diag.Range.Start.Line 5 "Error should be in line 6" - }) - testCaseAsync "can load other file in project" (async { - let! (doc, diags) = server |> Server.openDocument "Program.fs" - use doc = doc - - Expect.exists diags (fun diag -> diag.Message.Contains "The value or constructor 'programBar' is not defined." && diag.Range.Start.Line = 4) "Should be not defined error" - // `argv` - Expect.exists diags (fun diag -> diag.Message.Contains "This value is unused" && diag.Range.Start.Line = 11) "Should be unused value" - Expect.exists diags (fun diag -> diag.Message.Contains "Unused open statement" && diag.Range.Start.Line = 2) "Should be unused open" - }) - ]) - ] - ] - - testList "Waiting for diagnostics" [ - let allAnalyzersConfig = - { defaultConfigDto with - UnusedOpensAnalyzer = Some true - UnusedDeclarationsAnalyzer = Some true - SimplifyNameAnalyzer = Some true - } - serverTestList "waitForLatestDiagnostics" state allAnalyzersConfig None (fun server -> [ - // `Document.waitForLatestDiagnostics` is crucial for success of tests: Must wait for newest, current Diagnostics, but ignore diags from previous parses. - // Issues: - // * must ignore old events - // * multiple `publishDiagnostics` for each parse - - // Test in here: a script with a lot of Analyzer Diagnostics: - // Analyzers are checked after F# Compiler Checking is done (-> already one `publishDiagnostics`) - // After analyzers `documentAnalyzed` gets sent. But might arrive before analyzer diags. - - let genSource nCompilerErrorsPerRepeat nUnusedOpensPerRepeat nUnusedDeclsPerRepeat nSimplifyNamesPerRepeat repeats identifier = - // generate source with lots of Analyzer Diagnostics (and F# compiler errors) - // identifier to force some textual changes - let nss = [| - "System" - "System.Diagnostics" - "System.Text" - "System.Text.RegularExpressions" - "System.Threading" - "System.Runtime" - "FSharp.Control" - "FSharp.Linq" - "FSharp.Quotations" - "FSharp.Reflection" - |] - let tys = [| - "System.String" - "System.Index" - "System.Int32" - "System.Random" - "System.Guid" - "System.Text.RegularExpressions.Regex" - "System.Text.RegularExpressions.Match" - "System.Text.StringBuilder" - "System.Diagnostics.TraceLevel" - "System.Diagnostics.Stopwatch" - |] - - let lines = [ - $"// {identifier}" - for i in 1..repeats do - $"// Rep {i}" - for j in 1..nUnusedOpensPerRepeat do - let o = Array.get nss ((j-1) % nss.Length) - $"open {o}" - - for j in 1..nUnusedDeclsPerRepeat do - $"let {identifier}Rep{i}Val{j} = 0" - - // note: requires at least 4 UnusedOpens (to `open ...` required for Simplify Name) - for j in 1..nSimplifyNamesPerRepeat do - let ty = Array.get tys ((j-1) % tys.Length ) - $"let _{identifier}Rep{i}F{j} (v: {ty}) = v" - - // `let _identifier = value`: - // * value not defined - // * no unused warning because `_` - for j in 1..nCompilerErrorsPerRepeat do - $"let _{identifier}ErrorRep{i}Val{j} = valueRep{i}Val{j}" - - "" - ] - - String.concat "\n" lines - - testCaseAsync "lots of diagnostics for all analyzers" (async { - // count for each: n * repeats - let nCompilerErrorsPerRepeat = 3 - let nUnusedOpensPerRepeat = 7 - let nUnusedDeclsPerRepeat = 5 - let nSimplifyNamesPerRepeat = 9 - let calcExpected repeats = - {| - UnusedOpens = nUnusedOpensPerRepeat * repeats - UnusedDecls = nUnusedDeclsPerRepeat * repeats - SimplifyNames = nSimplifyNamesPerRepeat * repeats - CompilerErrors = nCompilerErrorsPerRepeat * repeats - |} - - let repeats = 2 - let source = genSource nCompilerErrorsPerRepeat nUnusedOpensPerRepeat nUnusedDeclsPerRepeat nSimplifyNamesPerRepeat repeats "init" - let! (doc, diags) = server |> Server.createUntitledDocument source - use doc = doc - - let checkDiags repeats loop diags = - let expected = calcExpected repeats - let groups = - diags - |> Array.map (fun d -> - // simplify `The value or constructor 'value' is not defined.` error (contains names and recommendations) - if d.Code = Some "39" then - "The value or constructor is not defined" - else - d.Message - ) - |> Array.countBy id - |> Map.ofArray - let actual = {| - UnusedOpens = groups.["Unused open statement"] - UnusedDecls = groups.["This value is unused"] - SimplifyNames = groups.["This qualifier is redundant"] - CompilerErrors = groups.["The value or constructor is not defined"] - |} - - // exact count isn't actually that important because each analyzers sends all its diags together. - // important part is just: has arrived -> `waitForLatestDiagnostics` waited long enough for all diags - Expect.equal actual expected $"Incorrect dags in loop {loop}" - - checkDiags repeats 0 diags - - for i in 1..5 do - let repeats = repeats + i // to get different numbers of diagnostics - let source = genSource nCompilerErrorsPerRepeat nUnusedOpensPerRepeat nUnusedDeclsPerRepeat nSimplifyNamesPerRepeat repeats $"loop{i}" - let! diags = doc |> Document.changeTextTo source - checkDiags repeats i diags - }) - - testCaseAsync "diagnostics for some analyzers" (async { - let checkDiags (unusedOpen, unusedValue, simplifyName) diags = - let actual = {| - UnusedOpen = diags |> Array.exists (fun d -> d.Message = "Unused open statement") - UnusedDecl = diags |> Array.exists (fun d -> d.Message = "This value is unused") - SimplifyName = diags |> Array.exists (fun d -> d.Message = "This qualifier is redundant") - |} - let expected = {| - UnusedOpen = unusedOpen - UnusedDecl = unusedValue - SimplifyName = simplifyName - |} - - Expect.equal actual expected "Should contain correct diagnostics" - - let source = Text.trimTripleQuotation """ +let tests state = + testList + (nameof (Server)) + [ testList + "no root path" + [ testList + "can get diagnostics" + [ let config = + { defaultConfigDto with + UnusedOpensAnalyzer = Some false + UnusedDeclarationsAnalyzer = Some false + SimplifyNameAnalyzer = Some false } + + serverTestList "no analyzers" state config None (fun server -> + [ testCaseAsync + "can get nothing wrong" + (async { + let! (doc, diags) = server |> Server.createUntitledDocument "" + use doc = doc + Expect.isEmpty diags "There should be no diagnostics" + + for i in 1..5 do + let! diags = doc |> Document.changeTextTo (string i) + Expect.isEmpty diags "There should be no diagnostics" + }) + testCaseAsync + "can get single error" + (async { + let! (doc, diags) = server |> Server.createUntitledDocument "let foo = notdefined" + use doc = doc + Expect.hasLength diags 1 "There should be 1 error" + Expect.exists diags (fun d -> d.Message.Contains "notdefined") "" + let! diags = doc |> Document.changeTextTo "let bar = doesnotexist" + Expect.hasLength diags 1 "There should be 1 error" + Expect.exists diags (fun d -> d.Message.Contains "doesnotexist") "" + let! diags = doc |> Document.changeTextTo "let baz = nope" + Expect.hasLength diags 1 "There should be 1 error" + Expect.exists diags (fun d -> d.Message.Contains "nope") "" + }) + testCaseAsync + "can get multiple errors" + (async { + let source = "let foo = {0}\nlet bar = {1}\nlet baz = {2}" + let names = [| "notdefined"; "doesnotexist"; "nope" |] + let fnames i = names |> Array.map (fun n -> sprintf "%s%i" n i) + let fsource i = String.Format(source, fnames i |> Seq.cast |> Seq.toArray) + + let! (doc, diags) = server |> Server.createUntitledDocument (fsource 0) + use doc = doc + Expect.hasLength diags (names.Length) "" + + for name in fnames 0 do + Expect.exists diags (fun d -> d.Message.Contains name) "" + + for i in 1..2 do + let! diags = doc |> Document.changeTextTo (fsource i) + Expect.hasLength diags (names.Length) "" + + for name in fnames i do + Expect.exists diags (fun d -> d.Message.Contains name) "" + }) ]) + + let config = + { defaultConfigDto with + UnusedOpensAnalyzer = Some false + UnusedDeclarationsAnalyzer = Some true + SimplifyNameAnalyzer = Some false } + + serverTestList "just unused decl analyzer" state config None (fun server -> + [ testCaseAsync "can get nothing wrong" + <| (async { + let! (doc, diags) = server |> Server.createUntitledDocument "" + use doc = doc + Expect.isEmpty diags "There should be no diagnostics" + + for i in 1..5 do + let! diags = doc |> Document.changeTextTo (string i) + Expect.isEmpty diags "There should be no diagnostics" + }) + testCaseAsync + "can get diags for single line" + (async { + let! (doc, diags) = server |> Server.createUntitledDocument "let foo = notdefined" + use doc = doc + Expect.hasLength diags 2 "" + Expect.exists diags (fun d -> d.Message.Contains "notdefined") "" + Expect.exists diags (fun d -> d.Message = "This value is unused") "" + let! diags = doc |> Document.changeTextTo "let bar = doesnotexist" + Expect.hasLength diags 2 "" + Expect.exists diags (fun d -> d.Message.Contains "doesnotexist") "" + Expect.exists diags (fun d -> d.Message = "This value is unused") "" + let! diags = doc |> Document.changeTextTo "let baz = nope" + Expect.hasLength diags 2 "" + Expect.exists diags (fun d -> d.Message.Contains "nope") "" + Expect.exists diags (fun d -> d.Message = "This value is unused") "" + }) + testCaseAsync + "can get diags for multiple lines" + (async { + let nVars = 3 + let values i = Array.init nVars (sprintf "someValue%i%i" i) + let source i = values i |> Seq.mapi (sprintf "let var%i = %s") |> String.concat "\n" + + let! (doc, diags) = server |> Server.createUntitledDocument (source 0) + use doc = doc + Expect.hasLength diags (nVars * 2) "" + + values 0 + |> Array.iteri (fun i name -> + Expect.exists diags (fun d -> d.Message.Contains name) $"No diags with name {name}" + + Expect.exists + diags + (fun d -> d.Message = "This value is unused" && d.Range.Start.Line = i) + $"No unused value error in line {i}") + + for i in 1..2 do + let! diags = doc |> Document.changeTextTo (source i) + Expect.hasLength diags (nVars * 2) "" + + values i + |> Array.iteri (fun i name -> + Expect.exists diags (fun d -> d.Message.Contains name) $"No diags with name {name}" + + Expect.exists + diags + (fun d -> d.Message = "This value is unused" && d.Range.Start.Line = i) + $"No unused value error in line {i}") + }) ]) + + let config = + { defaultConfigDto with + UnusedOpensAnalyzer = Some true + UnusedDeclarationsAnalyzer = Some true + SimplifyNameAnalyzer = Some true } + + serverTestList "three analyzers" state config None (fun server -> + [ testCaseAsync + "can get nothing wrong" + (async { + let! (doc, diags) = server |> Server.createUntitledDocument "" + use doc = doc + Expect.isEmpty diags "There should be no diagnostics" + + for i in 1..5 do + let! diags = doc |> Document.changeTextTo (string i) + Expect.isEmpty diags "There should be no diagnostics" + }) + testCaseAsync + "can get all diags" + (async { + let source = "open System\nlet foo = bar\nSystem.String.Empty |> ignore" + let! (doc, diags) = server |> Server.createUntitledDocument source + use doc = doc + + Expect.hasLength diags 4 "" + Expect.exists diags (fun d -> d.Message = "Unused open statement" && d.Range.Start.Line = 0) "" + Expect.exists diags (fun d -> d.Message = "This value is unused" && d.Range.Start.Line = 1) "" + Expect.exists diags (fun d -> d.Message.Contains "bar" && d.Range.Start.Line = 1) "" + + Expect.exists + diags + (fun d -> d.Message = "This qualifier is redundant" && d.Range.Start.Line = 2) + "" + + + let source = + "open System.Collections\nlet baz = foo\nSystem.Collections.Generic.List() |> ignore" + + let! diags = doc |> Document.changeTextTo source + + Expect.hasLength diags 4 "" + Expect.exists diags (fun d -> d.Message = "Unused open statement" && d.Range.Start.Line = 0) "" + Expect.exists diags (fun d -> d.Message = "This value is unused" && d.Range.Start.Line = 1) "" + Expect.exists diags (fun d -> d.Message.Contains "foo" && d.Range.Start.Line = 1) "" + + Expect.exists + diags + (fun d -> d.Message = "This qualifier is redundant" && d.Range.Start.Line = 2) + "" + + + let source = + "open System.Diagnostics\nlet bar = baz\nSystem.Diagnostics.Debugger.IsAttached" + + let! diags = doc |> Document.changeTextTo source + + Expect.hasLength diags 4 "" + Expect.exists diags (fun d -> d.Message = "Unused open statement" && d.Range.Start.Line = 0) "" + Expect.exists diags (fun d -> d.Message = "This value is unused" && d.Range.Start.Line = 1) "" + Expect.exists diags (fun d -> d.Message.Contains "baz" && d.Range.Start.Line = 1) "" + + Expect.exists + diags + (fun d -> d.Message = "This qualifier is redundant" && d.Range.Start.Line = 2) + "" + }) ]) ] + + testList + "untitled document" + [ serverTestList + "untitled counter in server for createUntitledDocument" + state + defaultConfigDto + None + (fun server -> + [ testCaseAsync + "creating document increases untitled counter" + (async { + let! actualServer = server + let preCounter = actualServer.UntitledCounter + let! (doc, _) = server |> Server.createUntitledDocument "" + use _doc = doc + let postCounter = actualServer.UntitledCounter + + Expect.isGreaterThan postCounter preCounter "Untitled Counter should increase" + }) + testCaseAsync + "creating multiple documents increases untitled counter" + (async { + let getCounter server = server |> Async.map (fun s -> s.UntitledCounter) + + let! preCounter = getCounter server + let mutable preCounter = preCounter + + for _ in 1..5 do + let! (doc, _) = server |> Server.createUntitledDocument "" + use _doc = doc + let! postCounter = getCounter server + Expect.isGreaterThan postCounter preCounter "Untitled Counter should increase" + preCounter <- postCounter + }) ]) + + serverTestList "document version" state defaultConfigDto None (fun server -> + [ testCaseAsync + "changing document text increases document version" + (async { + let! (doc, _) = server |> Server.createUntitledDocument "" + let preVersion = doc.Version + let! _ = doc |> Document.changeTextTo "42" + let postVersion = doc.Version + + Expect.isGreaterThan postVersion preVersion "Document Version should increase" + }) + testCaseAsync + "changing document text multiple times should always increase document version" + (async { + let! (doc, _) = server |> Server.createUntitledDocument "" + let mutable preVersion = doc.Version + + for _ in 1..5 do + let! _ = doc |> Document.changeTextTo "" + let postVersion = doc.Version + Expect.isGreaterThan postVersion preVersion "Document Version should increase" + preVersion <- postVersion + }) ]) ] ] + + testList + "with root path" + [ let inTestCases name = + System.IO.Path.Combine(__SOURCE_DIRECTORY__, "..", "TestCases", "ServerTests", name) + |> Some + + let noAnalyzersConfig = + { defaultConfigDto with + UnusedOpensAnalyzer = Some false + UnusedDeclarationsAnalyzer = Some false + SimplifyNameAnalyzer = Some false } + + let allAnalyzersConfig = + { defaultConfigDto with + UnusedOpensAnalyzer = Some true + UnusedDeclarationsAnalyzer = Some true + SimplifyNameAnalyzer = Some true } + + serverTestList + "dir with just a script and no analyzers" + state + noAnalyzersConfig + (inTestCases "JustScript") + (fun server -> + [ testCaseAsync + "can load script file" + (async { + let! (doc, diags) = server |> Server.openDocument "Script.fsx" + use _doc = doc + + Expect.hasLength diags 1 "Should be one diagnostics" + let diag = diags |> Array.head + + Expect.stringContains + diag.Message + "The value or constructor 'bar' is not defined." + "Should be not defined error" + + Expect.equal diag.Range.Start.Line 0 "Error should be in line 1" + }) + testCaseAsync + "can load script file again" + (async { + let! (doc, diags) = server |> Server.openDocument "Script.fsx" + use _doc = doc + + Expect.hasLength diags 1 "Should be one diagnostics" + let diag = diags |> Array.head + + Expect.stringContains + diag.Message + "The value or constructor 'bar' is not defined." + "Should be not defined error" + + Expect.equal diag.Range.Start.Line 0 "Error should be in line 1" + }) ]) + + serverTestList + "dir with just a script and all anaylzers" + state + allAnalyzersConfig + (inTestCases "JustScript") + (fun server -> + [ testCaseAsync + "can load script file" + (async { + let! (doc, diags) = server |> Server.openDocument "Script.fsx" + use _doc = doc + + Expect.exists + diags + (fun diag -> + diag.Message.Contains "The value or constructor 'bar' is not defined." + && diag.Range.Start.Line = 0) + "Should be not defined error" + + Expect.exists + diags + (fun diag -> diag.Message.Contains "This value is unused" && diag.Range.Start.Line = 0) + "Should be unused value" + }) + testCaseAsync + "can load script file again" + (async { + let! (doc, diags) = server |> Server.openDocument "Script.fsx" + use _doc = doc + + Expect.exists + diags + (fun diag -> + diag.Message.Contains "The value or constructor 'bar' is not defined." + && diag.Range.Start.Line = 0) + "Should be not defined error" + + Expect.exists + diags + (fun diag -> diag.Message.Contains "This value is unused" && diag.Range.Start.Line = 0) + "Should be unused value" + }) ]) + + testSequenced + <| testList + "contesting" + [ let projectDir = inTestCases "Project" + + serverTestList "dir with project and no analyzers" state noAnalyzersConfig projectDir (fun server -> + [ testCaseAsync + "can load file in project" + (async { + let! (doc, diags) = server |> Server.openDocument "Other.fs" + use _doc = doc + + Expect.hasLength diags 1 "Should be one diagnostics" + let diag = diags |> Array.head + + Expect.stringContains + diag.Message + "The value or constructor 'otherBar' is not defined." + "Should be not defined error" + + Expect.equal diag.Range.Start.Line 5 "Error should be in line 6" + }) + testCaseAsync + "can load file in project again" + (async { + let! (doc, diags) = server |> Server.openDocument "Other.fs" + use _doc = doc + + Expect.hasLength diags 1 "Should be one diagnostics" + let diag = diags |> Array.head + + Expect.stringContains + diag.Message + "The value or constructor 'otherBar' is not defined." + "Should be not defined error" + + Expect.equal diag.Range.Start.Line 5 "Error should be in line 6" + }) + testCaseAsync + "can load other file in project" + (async { + let! (doc, diags) = server |> Server.openDocument "Program.fs" + use _doc = doc + + Expect.hasLength diags 1 "Should be one diagnostics" + let diag = diags |> Array.head + + Expect.stringContains + diag.Message + "The value or constructor 'programBar' is not defined." + "Should be not defined error" + + Expect.equal diag.Range.Start.Line 4 "Error should be in line 5" + }) ]) + + serverTestList "dir with project and all analyzers" state allAnalyzersConfig projectDir (fun server -> + [ testCaseAsync + "can load file in project" + (async { + let! (doc, diags) = server |> Server.openDocument "Other.fs" + use _doc = doc + + Expect.hasLength diags 1 "Should be one diagnostics" + let diag = diags |> Array.head + + Expect.stringContains + diag.Message + "The value or constructor 'otherBar' is not defined." + "Should be not defined error" + + Expect.equal diag.Range.Start.Line 5 "Error should be in line 6" + }) + testCaseAsync + "can load file in project again" + (async { + let! (doc, diags) = server |> Server.openDocument "Other.fs" + use _doc = doc + + Expect.hasLength diags 1 "Should be one diagnostics" + let diag = diags |> Array.head + + Expect.stringContains + diag.Message + "The value or constructor 'otherBar' is not defined." + "Should be not defined error" + + Expect.equal diag.Range.Start.Line 5 "Error should be in line 6" + }) + testCaseAsync + "can load other file in project" + (async { + let! (doc, diags) = server |> Server.openDocument "Program.fs" + use _doc = doc + + Expect.exists + diags + (fun diag -> + diag.Message.Contains "The value or constructor 'programBar' is not defined." + && diag.Range.Start.Line = 4) + "Should be not defined error" + // `argv` + Expect.exists + diags + (fun diag -> diag.Message.Contains "This value is unused" && diag.Range.Start.Line = 11) + "Should be unused value" + + Expect.exists + diags + (fun diag -> diag.Message.Contains "Unused open statement" && diag.Range.Start.Line = 2) + "Should be unused open" + }) ]) ] ] + + testList + "Waiting for diagnostics" + [ let allAnalyzersConfig = + { defaultConfigDto with + UnusedOpensAnalyzer = Some true + UnusedDeclarationsAnalyzer = Some true + SimplifyNameAnalyzer = Some true } + + serverTestList "waitForLatestDiagnostics" state allAnalyzersConfig None (fun server -> + [ + // `Document.waitForLatestDiagnostics` is crucial for success of tests: Must wait for newest, current Diagnostics, but ignore diags from previous parses. + // Issues: + // * must ignore old events + // * multiple `publishDiagnostics` for each parse + + // Test in here: a script with a lot of Analyzer Diagnostics: + // Analyzers are checked after F# Compiler Checking is done (-> already one `publishDiagnostics`) + // After analyzers `documentAnalyzed` gets sent. But might arrive before analyzer diags. + + let genSource + nCompilerErrorsPerRepeat + nUnusedOpensPerRepeat + nUnusedDeclsPerRepeat + nSimplifyNamesPerRepeat + repeats + identifier + = + // generate source with lots of Analyzer Diagnostics (and F# compiler errors) + // identifier to force some textual changes + let nss = + [| "System" + "System.Diagnostics" + "System.Text" + "System.Text.RegularExpressions" + "System.Threading" + "System.Runtime" + "FSharp.Control" + "FSharp.Linq" + "FSharp.Quotations" + "FSharp.Reflection" |] + + let tys = + [| "System.String" + "System.Index" + "System.Int32" + "System.Random" + "System.Guid" + "System.Text.RegularExpressions.Regex" + "System.Text.RegularExpressions.Match" + "System.Text.StringBuilder" + "System.Diagnostics.TraceLevel" + "System.Diagnostics.Stopwatch" |] + + let lines = + [ $"// {identifier}" + for i in 1..repeats do + $"// Rep {i}" + + for j in 1..nUnusedOpensPerRepeat do + let o = Array.get nss ((j - 1) % nss.Length) + $"open {o}" + + for j in 1..nUnusedDeclsPerRepeat do + $"let {identifier}Rep{i}Val{j} = 0" + + // note: requires at least 4 UnusedOpens (to `open ...` required for Simplify Name) + for j in 1..nSimplifyNamesPerRepeat do + let ty = Array.get tys ((j - 1) % tys.Length) + $"let _{identifier}Rep{i}F{j} (v: {ty}) = v" + + // `let _identifier = value`: + // * value not defined + // * no unused warning because `_` + for j in 1..nCompilerErrorsPerRepeat do + $"let _{identifier}ErrorRep{i}Val{j} = valueRep{i}Val{j}" + + "" ] + + String.concat "\n" lines + + testCaseAsync + "lots of diagnostics for all analyzers" + (async { + // count for each: n * repeats + let nCompilerErrorsPerRepeat = 3 + let nUnusedOpensPerRepeat = 7 + let nUnusedDeclsPerRepeat = 5 + let nSimplifyNamesPerRepeat = 9 + + let calcExpected repeats = + {| UnusedOpens = nUnusedOpensPerRepeat * repeats + UnusedDecls = nUnusedDeclsPerRepeat * repeats + SimplifyNames = nSimplifyNamesPerRepeat * repeats + CompilerErrors = nCompilerErrorsPerRepeat * repeats |} + + let repeats = 2 + + let source = + genSource + nCompilerErrorsPerRepeat + nUnusedOpensPerRepeat + nUnusedDeclsPerRepeat + nSimplifyNamesPerRepeat + repeats + "init" + + let! (doc, diags) = server |> Server.createUntitledDocument source + use doc = doc + + let checkDiags repeats loop diags = + let expected = calcExpected repeats + + let groups = + diags + |> Array.map (fun d -> + // simplify `The value or constructor 'value' is not defined.` error (contains names and recommendations) + if d.Code = Some "39" then + "The value or constructor is not defined" + else + d.Message) + |> Array.countBy id + |> Map.ofArray + + let actual = + {| UnusedOpens = groups.["Unused open statement"] + UnusedDecls = groups.["This value is unused"] + SimplifyNames = groups.["This qualifier is redundant"] + CompilerErrors = groups.["The value or constructor is not defined"] |} + + // exact count isn't actually that important because each analyzers sends all its diags together. + // important part is just: has arrived -> `waitForLatestDiagnostics` waited long enough for all diags + Expect.equal actual expected $"Incorrect dags in loop {loop}" + + checkDiags repeats 0 diags + + for i in 1..5 do + let repeats = repeats + i // to get different numbers of diagnostics + + let source = + genSource + nCompilerErrorsPerRepeat + nUnusedOpensPerRepeat + nUnusedDeclsPerRepeat + nSimplifyNamesPerRepeat + repeats + $"loop{i}" + + let! diags = doc |> Document.changeTextTo source + checkDiags repeats i diags + }) + + testCaseAsync + "diagnostics for some analyzers" + (async { + let checkDiags (unusedOpen, unusedValue, simplifyName) diags = + let actual = + {| UnusedOpen = diags |> Array.exists (fun d -> d.Message = "Unused open statement") + UnusedDecl = diags |> Array.exists (fun d -> d.Message = "This value is unused") + SimplifyName = diags |> Array.exists (fun d -> d.Message = "This qualifier is redundant") |} + + let expected = + {| UnusedOpen = unusedOpen + UnusedDecl = unusedValue + SimplifyName = simplifyName |} + + Expect.equal actual expected "Should contain correct diagnostics" + + let source = + Text.trimTripleQuotation + """ open System open System.Diagnostics open System.Text @@ -485,39 +661,51 @@ let x = 1 let y = 2 let z = 3 """ - let! (doc, diags) = server |> Server.createUntitledDocument source - use doc = doc - checkDiags (true, true, false) diags - let source = Text.trimTripleQuotation """ + let! (doc, diags) = server |> Server.createUntitledDocument source + use doc = doc + checkDiags (true, true, false) diags + + let source = + Text.trimTripleQuotation + """ let x = 1 let y = 2 let z = 3 """ - let! diags = doc |> Document.changeTextTo source - checkDiags (false, true, false) diags - let source = Text.trimTripleQuotation """ + let! diags = doc |> Document.changeTextTo source + checkDiags (false, true, false) diags + + let source = + Text.trimTripleQuotation + """ open System open System.Diagnostics open System.Text () """ - let! diags = doc |> Document.changeTextTo source - checkDiags (true, false, false) diags - let source = Text.trimTripleQuotation """ + let! diags = doc |> Document.changeTextTo source + checkDiags (true, false, false) diags + + let source = + Text.trimTripleQuotation + """ open System open System.Diagnostics open System.Text let _f (v: System.String) = v """ - let! diags = doc |> Document.changeTextTo source - checkDiags (true, false, true) diags - let source = Text.trimTripleQuotation """ + let! diags = doc |> Document.changeTextTo source + checkDiags (true, false, true) diags + + let source = + Text.trimTripleQuotation + """ open System open System.Diagnostics open System.Text @@ -525,24 +713,26 @@ open System.Text let f (v: System.String) = () """ - let! diags = doc |> Document.changeTextTo source - checkDiags (true, true, true) diags - - let source = "()" - let! diags = doc |> Document.changeTextTo source - checkDiags (false, false, false) diags - }) - ]) - ] - - testList "timing" [ - let allAnalyzersConfig = - { defaultConfigDto with - UnusedOpensAnalyzer = Some true - UnusedDeclarationsAnalyzer = Some true - SimplifyNameAnalyzer = Some true - } - let mkSource (msg: string) = Text.trimTripleQuotation $""" + + let! diags = doc |> Document.changeTextTo source + checkDiags (true, true, true) diags + + let source = "()" + let! diags = doc |> Document.changeTextTo source + checkDiags (false, false, false) diags + }) ]) ] + + testList + "timing" + [ let allAnalyzersConfig = + { defaultConfigDto with + UnusedOpensAnalyzer = Some true + UnusedDeclarationsAnalyzer = Some true + SimplifyNameAnalyzer = Some true } + + let mkSource (msg: string) = + Text.trimTripleQuotation + $""" open System // {msg} @@ -557,113 +747,147 @@ f1 foo // {msg} f2 "bar" |> ignore """ - serverTestList "server" state allAnalyzersConfig None (fun server -> [ - testList "single parse" [ - testCaseAsync "single parse" <| async { - let! (doc, _) = server |> Server.createUntitledDocument (mkSource "single parse") - use doc = doc - () - } - ] - testList "parse of same document" [ - testCaseAsync "single doc" <| async { - let! (doc, _) = server |> Server.createUntitledDocument (mkSource "0 parse") - use doc = doc - - for i in 1..5 do - let! _ = doc |> Document.changeTextTo (mkSource $"Parse {i}") - () - () - } - ] - testList "parse in different documents" [ - for i in 0..5 do - testCaseAsync $"doc {i}" <| async { - let! (doc, _) = server |> Server.createUntitledDocument (mkSource "parse {i}") - use doc = doc - () - } - ] - ]) - ] - - testList "Document" [ - serverTestList "no root path without analyzers" state defaultConfigDto None (fun server -> [ - testCaseAsync "can create Document by absolute path without root path" <| async { - let relativePath = "../TestCases/ServerTests/JustScript/Script.fsx" - let absolutePath = System.IO.Path.GetFullPath(System.IO.Path.Combine(__SOURCE_DIRECTORY__, relativePath)) - let! (doc, _) = server |> Server.openDocument absolutePath - use doc = doc - () - } - - let mutable docState = {| Uri = ""; Version = -1; CallCounter = 0 |} - let getDoc server = async { - let text = Text.trimTripleQuotation """ + + serverTestList "server" state allAnalyzersConfig None (fun server -> + [ testList + "single parse" + [ testCaseAsync "single parse" + <| async { + let! (doc, _) = server |> Server.createUntitledDocument (mkSource "single parse") + use _doc = doc + () + } ] + testList + "parse of same document" + [ testCaseAsync "single doc" + <| async { + let! (doc, _) = server |> Server.createUntitledDocument (mkSource "0 parse") + use doc = doc + + for i in 1..5 do + let! _ = doc |> Document.changeTextTo (mkSource $"Parse {i}") + () + + () + } ] + testList + "parse in different documents" + [ for i in 0..5 do + testCaseAsync $"doc {i}" + <| async { + let! (doc, _) = server |> Server.createUntitledDocument (mkSource "parse {i}") + use _doc = doc + () + } ] ]) ] + + testList + "Document" + [ serverTestList "no root path without analyzers" state defaultConfigDto None (fun server -> + [ testCaseAsync "can create Document by absolute path without root path" + <| async { + let relativePath = "../TestCases/ServerTests/JustScript/Script.fsx" + + let absolutePath = + System.IO.Path.GetFullPath(System.IO.Path.Combine(__SOURCE_DIRECTORY__, relativePath)) + + let! (doc, _) = server |> Server.openDocument absolutePath + use _doc = doc + () + } + + let mutable docState = + {| Uri = "" + Version = -1 + CallCounter = 0 |} + + let getDoc server = + async { + let text = + Text.trimTripleQuotation + """ let bar = "hello world" let foo = System.String. """ - let! (doc, diags) = server |> Server.createUntitledDocument text - docState <- {| - Uri = doc.Uri - Version = doc.Version - // tracks how often `getDoc` was called - CallCounter = docState.CallCounter + 1 - |} - return (doc, diags) - } - documentTestList "multiple actions on single document" server getDoc (fun doc -> [ - testCaseAsync "doc is doc returned from getDocument" <| async { - let! (doc,_) = doc - Expect.equal (doc.Uri, doc.Version) (docState.Uri, docState.Version) "Should be same doc" - Expect.equal docState.CallCounter 1 "getDocument should only be called once" - } - testCaseAsync "doc stays same" <| async { - let! (doc,_) = doc - Expect.equal (doc.Uri, doc.Version) (docState.Uri, docState.Version) "Should be same doc" - Expect.equal docState.CallCounter 1 "getDocument should only be called once" - } - let completionAt pos (doc: Document) = async { - let ps: CompletionParams = { - TextDocument = doc.TextDocumentIdentifier - Position = pos - Context = None - } - let! res = doc.Server.Server.TextDocumentCompletion ps - Expect.isOk res "Should be ok result" - return res |> Result.defaultWith (fun _ -> failtest "unreachable") - } - testCaseAsync "can get completions" <| async { - let! (doc,_) = doc - let! completions = doc |> completionAt { Line = 1; Character = 24 } - Expect.isSome completions "Should be some completions" - let completions = completions.Value - Expect.isNonEmpty completions.Items "Should be completions" - Expect.exists completions.Items (fun i -> i.Label = "IsNullOrWhiteSpace") "Should have `IsNullOrWhiteSpace` completion" - } - testCaseAsync "can get completions again" <| async { - let! (doc,_) = doc - let! completions = doc |> completionAt { Line = 1; Character = 24 } - Expect.isSome completions "Should be some completions" - let completions = completions.Value - Expect.isNonEmpty completions.Items "Should be completions" - Expect.exists completions.Items (fun i -> i.Label = "IsNullOrWhiteSpace") "Should have `IsNullOrWhiteSpace` completion" - } - testCaseAsync "can get signature help" <| async { - let! (doc,_) = doc - let ps: TextDocumentPositionParams = { - TextDocument = doc.TextDocumentIdentifier - Position = { Line = 0; Character = 6 } - } - let! res = doc.Server.Server.TextDocumentHover ps - Expect.isOk res "Should have hover data" - } - testCaseAsync "doc is still same" <| async { - let! (doc,_) = doc - Expect.equal (doc.Uri, doc.Version) (docState.Uri, docState.Version) "Should be same doc" - Expect.equal docState.CallCounter 1 "getDocument should only be called once" - } - ]) - ]) - ] -] + + let! (doc, diags) = server |> Server.createUntitledDocument text + + docState <- + {| Uri = doc.Uri + Version = doc.Version + // tracks how often `getDoc` was called + CallCounter = docState.CallCounter + 1 |} + + return (doc, diags) + } + + documentTestList "multiple actions on single document" server getDoc (fun doc -> + [ testCaseAsync "doc is doc returned from getDocument" + <| async { + let! (doc, _) = doc + Expect.equal (doc.Uri, doc.Version) (docState.Uri, docState.Version) "Should be same doc" + Expect.equal docState.CallCounter 1 "getDocument should only be called once" + } + testCaseAsync "doc stays same" + <| async { + let! (doc, _) = doc + Expect.equal (doc.Uri, doc.Version) (docState.Uri, docState.Version) "Should be same doc" + Expect.equal docState.CallCounter 1 "getDocument should only be called once" + } + let completionAt pos (doc: Document) = + async { + let ps: CompletionParams = + { TextDocument = doc.TextDocumentIdentifier + Position = pos + Context = None } + + let! res = doc.Server.Server.TextDocumentCompletion ps + Expect.isOk res "Should be ok result" + return res |> Result.defaultWith (fun _ -> failtest "unreachable") + } + + testCaseAsync "can get completions" + <| async { + let! (doc, _) = doc + let! completions = doc |> completionAt { Line = 1; Character = 24 } + Expect.isSome completions "Should be some completions" + let completions = completions.Value + Expect.isNonEmpty completions.Items "Should be completions" + + Expect.exists + completions.Items + (fun i -> i.Label = "IsNullOrWhiteSpace") + "Should have `IsNullOrWhiteSpace` completion" + } + + testCaseAsync "can get completions again" + <| async { + let! (doc, _) = doc + let! completions = doc |> completionAt { Line = 1; Character = 24 } + Expect.isSome completions "Should be some completions" + let completions = completions.Value + Expect.isNonEmpty completions.Items "Should be completions" + + Expect.exists + completions.Items + (fun i -> i.Label = "IsNullOrWhiteSpace") + "Should have `IsNullOrWhiteSpace` completion" + } + + testCaseAsync "can get signature help" + <| async { + let! (doc, _) = doc + + let ps: TextDocumentPositionParams = + { TextDocument = doc.TextDocumentIdentifier + Position = { Line = 0; Character = 6 } } + + let! res = doc.Server.Server.TextDocumentHover ps + Expect.isOk res "Should have hover data" + } + + testCaseAsync "doc is still same" + <| async { + let! (doc, _) = doc + Expect.equal (doc.Uri, doc.Version) (docState.Uri, docState.Version) "Should be same doc" + Expect.equal docState.CallCounter 1 "getDocument should only be called once" + } ]) ]) ] ] diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/TextEdit.Tests.fs b/test/FsAutoComplete.Tests.Lsp/Utils/TextEdit.Tests.fs index e2af34854..aff5ff7a6 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/TextEdit.Tests.fs +++ b/test/FsAutoComplete.Tests.Lsp/Utils/TextEdit.Tests.fs @@ -1,4 +1,6 @@ module Utils.Tests.TextEdit + +open System open Expecto open Expecto.Logging open Expecto.Logging.Message @@ -7,551 +9,701 @@ open Ionide.LanguageServerProtocol.Types open Utils.TextEdit open Utils.Utils -let private logger = Expecto.Logging.Log.create (sprintf "%s.%s" (nameof Utils.Tests) (nameof Utils.TextEdit)) -let inline private pos line column: Position = { Line = line; Character = column } -let inline private range start fin = { Start = start; End = fin} +let private logger = + Expecto.Logging.Log.create (sprintf "%s.%s" (nameof Utils.Tests) (nameof Utils.TextEdit)) + +let inline private pos line column : Position = { Line = line; Character = column } +let inline private range start fin = { Start = start; End = fin } let inline private posRange pos = range pos pos let inline private (!-) text = Text.trimTripleQuotation text module private Cursor = open Expecto.Flip - let private tryExtractIndexTests = testList (nameof Cursor.tryExtractIndex) [ - testList "no cursor" [ - let assertNoCursor = - Cursor.tryExtractPosition - >> Expect.isNone "should have found no cursor" - testCase "empty string" <| fun _ -> - let text = "" - assertNoCursor text - testCase "single line" <| fun _ -> - let text = "Foo Bar Baz" - assertNoCursor text - testCase "two lines" <| fun _ -> - let text = "Foo Bar Baz\nLorem ipsum dolor sit" - assertNoCursor text - testCase "multiple lines" <| fun _ -> - let text = "Foo\nBar\nBaz\nLorem\nimpsum\ndolor\nsit" - assertNoCursor text - testCase "just spaces" <| fun _ -> - let text = " " - assertNoCursor text - testCase "just spaces and new lines" <| fun _ -> - let text = " \n \n\n \n\n\n\n \n \n \n" - assertNoCursor text - testCase "triple quoted string without processing" <| fun _ -> - let text = """ -module Foo - -let a = 42 -let b = + let private tryExtractIndexTests = + testList + (nameof Cursor.tryExtractIndex) + [ testList + "no cursor" + [ let assertNoCursor = + Cursor.tryExtractPosition >> Expect.isNone "should have found no cursor" + + testCase "empty string" + <| fun _ -> + let text = "" + assertNoCursor text + + testCase "single line" + <| fun _ -> + let text = "Foo Bar Baz" + assertNoCursor text + + testCase "two lines" + <| fun _ -> + let text = "Foo Bar Baz\nLorem ipsum dolor sit" + assertNoCursor text + + testCase "multiple lines" + <| fun _ -> + let text = "Foo\nBar\nBaz\nLorem\nimpsum\ndolor\nsit" + assertNoCursor text + + testCase "just spaces" + <| fun _ -> + let text = " " + assertNoCursor text + + testCase "just spaces and new lines" + <| fun _ -> + let text = " \n \n\n \n\n\n\n \n \n \n" + assertNoCursor text + + testCase "triple quoted string without processing" + <| fun _ -> + let text = + """ +module Foo + +let a = 42 +let b = a + 5 printfn "Result=%i" b """ - assertNoCursor text - testCase "triple quoted string with processing (starting new line, no indentation)" <| fun _ -> - let text = !- """ + + assertNoCursor text + + testCase "triple quoted string with processing (starting new line, no indentation)" + <| fun _ -> + let text = + !- """ module Foo let a = 42 -let b = +let b = a + 5 printfn "Result=%i" b """ - assertNoCursor text - testCase "triple quoted string with processing (starting new line, indentation)" <| fun _ -> - let text = !- """ + + assertNoCursor text + + testCase "triple quoted string with processing (starting new line, indentation)" + <| fun _ -> + let text = + !- """ module Foo let a = 42 - let b = + let b = a + 5 printfn "Result=%i" b """ - assertNoCursor text - ] - testList "with cursor" [ - let assertAndGetIndex = - Text.trimTripleQuotation - >> Cursor.tryExtractIndex - >> Option.defaultWith (fun _ -> failtest "No cursor found") - let assertResultIs (idx: int, text: string) = - assertAndGetIndex - >> Expect.equal "should be correct cursor position and text" (idx, text) - let assertCursorAt (idx: int) = - assertAndGetIndex - >> fst - >> Expect.equal "should have found cursor at correct position" idx - let assertTextIs (text: string) = - assertAndGetIndex - >> snd - >> Expect.equal "should have correct text" text - - testList "in normal string" [ - testCase "in empty string" <| fun _ -> - let text = "$0" - let expected = 0 - text |> assertCursorAt expected - testCase "start of single line" <| fun _ -> - let text = "$0Foo bar baz" - let expected = 0 - text |> assertCursorAt expected - testCase "end of single word" <| fun _ -> - let text = "foo$0" - // Note: out of string range: cursor is AFTER last character - let expected = 3 - text |> assertCursorAt expected - testCase "end of single line" <| fun _ -> - let text = "foo bar baz$0" - let expected = 11 - text |> assertCursorAt expected - testCase "removes cursor marker from single line" <| fun _ -> - let text = "foo $0bar" - let expected = "foo bar" - text |> assertTextIs expected - ] - testList "in triple quoted string" [ - testCase "in empty string unindented" <| fun _ -> - // technically incorrect: contains `\n` - let text = """ + + assertNoCursor text ] + testList + "with cursor" + [ let assertAndGetIndex = + Text.trimTripleQuotation + >> Cursor.tryExtractIndex + >> Option.defaultWith (fun _ -> failtest "No cursor found") + + let _assertResultIs (idx: int, text: string) = + assertAndGetIndex + >> Expect.equal "should be correct cursor position and text" (idx, text) + + let assertCursorAt (idx: int) = + assertAndGetIndex + >> fst + >> Expect.equal "should have found cursor at correct position" idx + + let assertTextIs (text: string) = assertAndGetIndex >> snd >> Expect.equal "should have correct text" text + + testList + "in normal string" + [ testCase "in empty string" + <| fun _ -> + let text = "$0" + let expected = 0 + text |> assertCursorAt expected + testCase "start of single line" + <| fun _ -> + let text = "$0Foo bar baz" + let expected = 0 + text |> assertCursorAt expected + testCase "end of single word" + <| fun _ -> + let text = "foo$0" + // Note: out of string range: cursor is AFTER last character + let expected = 3 + text |> assertCursorAt expected + testCase "end of single line" + <| fun _ -> + let text = "foo bar baz$0" + let expected = 11 + text |> assertCursorAt expected + testCase "removes cursor marker from single line" + <| fun _ -> + let text = "foo $0bar" + let expected = "foo bar" + text |> assertTextIs expected ] + + testList + "in triple quoted string" + [ testCase "in empty string unindented" + <| fun _ -> + // technically incorrect: contains `\n` + let text = + """ $0 """ - let expected = 0 - text |> assertCursorAt expected - testCase "in empty string indented" <| fun _ -> - // technically incorrect: contains `\n` - let text = """ + + let expected = 0 + text |> assertCursorAt expected + testCase "in empty string indented" + <| fun _ -> + // technically incorrect: contains `\n` + let text = + """ $0 """ - let expected = 0 - text |> assertCursorAt expected - testCase "in F# code unindented" <| fun _ -> - let text = """ + + let expected = 0 + text |> assertCursorAt expected + testCase "in F# code unindented" + <| fun _ -> + let text = + """ module Foo let $0a = 42 -let b = +let b = a + 5 printfn "Result=%i" b """ - let expected = 16 - text |> assertCursorAt expected - testCase "in F# code indented" <| fun _ -> - let text = """ + + let expected = 16 + text |> assertCursorAt expected + testCase "in F# code indented" + <| fun _ -> + let text = + """ module Foo let $0a = 42 - let b = + let b = a + 5 printfn "Result=%i" b """ - let expected = 16 - text |> assertCursorAt expected - testCase "removes cursor in F# code unindented" <| fun _ -> - let text = """ + + let expected = 16 + text |> assertCursorAt expected + testCase "removes cursor in F# code unindented" + <| fun _ -> + let text = + """ module Foo let $0a = 42 -let b = +let b = a + 5 printfn "Result=%i" b """ - // expected isn't trimmed in assertXXX -> do manually - let expected = !- """ + // expected isn't trimmed in assertXXX -> do manually + let expected = + !- """ module Foo let a = 42 -let b = +let b = a + 5 printfn "Result=%i" b """ - text |> assertTextIs expected - testCase "removes cursor in F# code indented" <| fun _ -> - let text = """ + + text |> assertTextIs expected + testCase "removes cursor in F# code indented" + <| fun _ -> + let text = + """ module Foo let $0a = 42 - let b = + let b = a + 5 printfn "Result=%i" b """ - let expected = !- """ + + let expected = + !- """ module Foo let a = 42 -let b = +let b = a + 5 printfn "Result=%i" b """ - text |> assertTextIs expected - testCase "finds and removes only first cursor" <| fun _ -> - let text = """ + + text |> assertTextIs expected + testCase "finds and removes only first cursor" + <| fun _ -> + let text = + """ module Foo let $0a = 42 - let $0b = + let $0b = a + 5 printfn "Result=%i" b """ - let expected = !- """ + + let expected = + !- """ module Foo let a = 42 -let $0b = +let $0b = a + 5 printfn "Result=%i" b """ - text |> assertTextIs expected - ] - ] - ] - - let private tryExtractPositionMarkedWithAnyOfTests = testList (nameof Cursor.tryExtractPositionMarkedWithAnyOf) [ - testCase "exact first of many cursors" <| fun _ -> - let text = "let $Avalue$B = $C42" - let actual = - text - |> Cursor.tryExtractPositionMarkedWithAnyOf [|"$B"; "$C"; "$A"|] - let expected = Some (("$A", pos 0 4), "let value$B = $C42") - - actual - |> Expect.equal "should be correct marker" expected - ] - - let private tryExtractPositionTests = testList (nameof Cursor.tryExtractPosition) [ - testList "no cursor" [ - let assertNoCursor = - Cursor.tryExtractPosition - >> Expect.isNone "should have found no cursor" - testCase "empty string" <| fun _ -> - let text = "" - assertNoCursor text - testCase "single line" <| fun _ -> - let text = "Foo Bar Baz" - assertNoCursor text - testCase "two lines" <| fun _ -> - let text = "Foo Bar Baz\nLorem ipsum dolor sit" - assertNoCursor text - testCase "multiple lines" <| fun _ -> - let text = "Foo\nBar\nBaz\nLorem\nimpsum\ndolor\nsit" - assertNoCursor text - testCase "just spaces" <| fun _ -> - let text = " " - assertNoCursor text - testCase "just spaces and new lines" <| fun _ -> - let text = " \n \n\n \n\n\n\n \n \n \n" - assertNoCursor text - testCase "triple quoted string without processing" <| fun _ -> - let text = """ -module Foo - -let a = 42 -let b = + + text |> assertTextIs expected ] ] ] + + let private tryExtractPositionMarkedWithAnyOfTests = + testList + (nameof Cursor.tryExtractPositionMarkedWithAnyOf) + [ testCase "exact first of many cursors" + <| fun _ -> + let text = "let $Avalue$B = $C42" + let actual = text |> Cursor.tryExtractPositionMarkedWithAnyOf [| "$B"; "$C"; "$A" |] + let expected = Some(("$A", pos 0 4), "let value$B = $C42") + + actual |> Expect.equal "should be correct marker" expected ] + + let private tryExtractPositionTests = + testList + (nameof Cursor.tryExtractPosition) + [ testList + "no cursor" + [ let assertNoCursor = + Cursor.tryExtractPosition >> Expect.isNone "should have found no cursor" + + testCase "empty string" + <| fun _ -> + let text = "" + assertNoCursor text + + testCase "single line" + <| fun _ -> + let text = "Foo Bar Baz" + assertNoCursor text + + testCase "two lines" + <| fun _ -> + let text = "Foo Bar Baz\nLorem ipsum dolor sit" + assertNoCursor text + + testCase "multiple lines" + <| fun _ -> + let text = "Foo\nBar\nBaz\nLorem\nimpsum\ndolor\nsit" + assertNoCursor text + + testCase "just spaces" + <| fun _ -> + let text = " " + assertNoCursor text + + testCase "just spaces and new lines" + <| fun _ -> + let text = " \n \n\n \n\n\n\n \n \n \n" + assertNoCursor text + + testCase "triple quoted string without processing" + <| fun _ -> + let text = + """ +module Foo + +let a = 42 +let b = a + 5 printfn "Result=%i" b """ - assertNoCursor text - testCase "triple quoted string with processing (starting new line, no indentation)" <| fun _ -> - let text = !- """ + + assertNoCursor text + + testCase "triple quoted string with processing (starting new line, no indentation)" + <| fun _ -> + let text = + !- """ module Foo let a = 42 -let b = +let b = a + 5 printfn "Result=%i" b """ - assertNoCursor text - testCase "triple quoted string with processing (starting new line, indentation)" <| fun _ -> - let text = !- """ + + assertNoCursor text + + testCase "triple quoted string with processing (starting new line, indentation)" + <| fun _ -> + let text = + !- """ module Foo let a = 42 - let b = + let b = a + 5 printfn "Result=%i" b """ - assertNoCursor text - ] - - testList "with cursor" [ - let assertAndGetCursor = - Text.trimTripleQuotation - >> Cursor.tryExtractPosition - >> Option.defaultWith (fun _ -> failtest "No cursor found") - let assertCursorAt (pos: Position) = - assertAndGetCursor - >> fst - >> Expect.equal "should have found cursor at correct position" pos - let assertTextIs (text: string) = - assertAndGetCursor - >> snd - >> Expect.equal "should have correct text" text - let assertResultIs (pos: Position, text: string) = - assertAndGetCursor - >> Expect.equal "should be correct cursor position and text" (pos, text) - - testList "in normal string" [ - testCase "in empty string" <| fun _ -> - let text = "$0" - let expected = pos 0 0 - text |> assertCursorAt expected - testCase "start of single line" <| fun _ -> - let text = "$0Foo bar baz" - let expected = pos 0 0 - text |> assertCursorAt expected - testCase "end of single word" <| fun _ -> - let text = "foo$0" - // Note: out of string range: cursor is AFTER last character - let expected = pos 0 3 - text |> assertCursorAt expected - testCase "end of single line" <| fun _ -> - let text = "foo bar baz$0" - let expected = pos 0 11 - text |> assertCursorAt expected - testCase "removes cursor marker from single line" <| fun _ -> - let text = "foo $0bar" - let expected = "foo bar" - text |> assertTextIs expected - ] - testList "in triple quoted string" [ - testCase "in empty string unindented" <| fun _ -> - // technically incorrect: contains `\n` - let text = """ + + assertNoCursor text ] + + testList + "with cursor" + [ let assertAndGetCursor = + Text.trimTripleQuotation + >> Cursor.tryExtractPosition + >> Option.defaultWith (fun _ -> failtest "No cursor found") + + let assertCursorAt (pos: Position) = + assertAndGetCursor + >> fst + >> Expect.equal "should have found cursor at correct position" pos + + let assertTextIs (text: string) = assertAndGetCursor >> snd >> Expect.equal "should have correct text" text + + let _assertResultIs (pos: Position, text: string) = + assertAndGetCursor + >> Expect.equal "should be correct cursor position and text" (pos, text) + + testList + "in normal string" + [ testCase "in empty string" + <| fun _ -> + let text = "$0" + let expected = pos 0 0 + text |> assertCursorAt expected + testCase "start of single line" + <| fun _ -> + let text = "$0Foo bar baz" + let expected = pos 0 0 + text |> assertCursorAt expected + testCase "end of single word" + <| fun _ -> + let text = "foo$0" + // Note: out of string range: cursor is AFTER last character + let expected = pos 0 3 + text |> assertCursorAt expected + testCase "end of single line" + <| fun _ -> + let text = "foo bar baz$0" + let expected = pos 0 11 + text |> assertCursorAt expected + testCase "removes cursor marker from single line" + <| fun _ -> + let text = "foo $0bar" + let expected = "foo bar" + text |> assertTextIs expected ] + + testList + "in triple quoted string" + [ testCase "in empty string unindented" + <| fun _ -> + // technically incorrect: contains `\n` + let text = + """ $0 """ - let expected = pos 0 0 - text |> assertCursorAt expected - testCase "in empty string indented" <| fun _ -> - // technically incorrect: contains `\n` - let text = """ + + let expected = pos 0 0 + text |> assertCursorAt expected + testCase "in empty string indented" + <| fun _ -> + // technically incorrect: contains `\n` + let text = + """ $0 """ - let expected = pos 0 0 - text |> assertCursorAt expected - testCase "in F# code unindented" <| fun _ -> - let text = """ + + let expected = pos 0 0 + text |> assertCursorAt expected + testCase "in F# code unindented" + <| fun _ -> + let text = + """ module Foo let $0a = 42 -let b = +let b = a + 5 printfn "Result=%i" b """ - let expected = pos 2 4 // 0-based, first line (with `"""`) is removed - text |> assertCursorAt expected - testCase "in F# code indented" <| fun _ -> - let text = """ + + let expected = pos 2 4 // 0-based, first line (with `"""`) is removed + text |> assertCursorAt expected + testCase "in F# code indented" + <| fun _ -> + let text = + """ module Foo let $0a = 42 - let b = + let b = a + 5 printfn "Result=%i" b """ - let expected = pos 2 4 // 0-based, first line (with `"""`) is removed, leading indentation removed - text |> assertCursorAt expected - testCase "removes cursor in F# code unindented" <| fun _ -> - let text = """ + + let expected = pos 2 4 // 0-based, first line (with `"""`) is removed, leading indentation removed + text |> assertCursorAt expected + testCase "removes cursor in F# code unindented" + <| fun _ -> + let text = + """ module Foo let $0a = 42 -let b = +let b = a + 5 printfn "Result=%i" b """ - // expected isn't trimmed in assertXXX -> do manually - let expected = !- """ + // expected isn't trimmed in assertXXX -> do manually + let expected = + !- """ module Foo let a = 42 -let b = +let b = a + 5 printfn "Result=%i" b """ - text |> assertTextIs expected - testCase "removes cursor in F# code indented" <| fun _ -> - let text = """ + + text |> assertTextIs expected + testCase "removes cursor in F# code indented" + <| fun _ -> + let text = + """ module Foo let $0a = 42 - let b = + let b = a + 5 printfn "Result=%i" b """ - let expected = !- """ + + let expected = + !- """ module Foo let a = 42 -let b = +let b = a + 5 printfn "Result=%i" b """ - text |> assertTextIs expected - testCase "finds and removes only first cursor" <| fun _ -> - let text = """ + + text |> assertTextIs expected + testCase "finds and removes only first cursor" + <| fun _ -> + let text = + """ module Foo let $0a = 42 - let $0b = + let $0b = a + 5 printfn "Result=%i" b """ - let expected = !- """ + + let expected = + !- """ module Foo let a = 42 -let $0b = +let $0b = a + 5 printfn "Result=%i" b """ - text |> assertTextIs expected - ] - ] - ] - - let tryExtractRangeTests = testList (nameof Cursor.tryExtractRange) [ - let assertAndGetRange = - Text.trimTripleQuotation - >> Cursor.tryExtractRange - >> Option.defaultWith (fun _ -> failtest "No cursor found") - let assertRangeIs (range: Range) = - assertAndGetRange - >> fst - >> Expect.equal "should have found correct range" range - let assertTextIs (text: string) = - assertAndGetRange - >> snd - >> Expect.equal "should have correct text" text - let assertResultIs (range: Range, text: string) = - assertAndGetRange - >> Expect.equal "should be correct range and text" (range, text) - testCase "no cursor results in no range" <| fun _ -> - let text = !- """ -module Foo - -let a = 42 -let b = + + text |> assertTextIs expected ] ] ] + + let tryExtractRangeTests = + testList + (nameof Cursor.tryExtractRange) + [ let assertAndGetRange = + Text.trimTripleQuotation + >> Cursor.tryExtractRange + >> Option.defaultWith (fun _ -> failtest "No cursor found") + + let assertRangeIs (range: Range) = + assertAndGetRange >> fst >> Expect.equal "should have found correct range" range + + let assertTextIs (text: string) = assertAndGetRange >> snd >> Expect.equal "should have correct text" text + + let assertResultIs (range: Range, text: string) = + assertAndGetRange + >> Expect.equal "should be correct range and text" (range, text) + + testCase "no cursor results in no range" + <| fun _ -> + let text = + !- """ +module Foo + +let a = 42 +let b = a + 5 printfn "Result=%i" b """ - text - |> Cursor.tryExtractRange - |> Expect.isNone "should have found no cursor" - testCase "can extract range in same line" <| fun _ -> - let text = """ + + text |> Cursor.tryExtractRange |> Expect.isNone "should have found no cursor" + + testCase "can extract range in same line" + <| fun _ -> + let text = + """ module Foo let $0a =$0 42 -let b = +let b = a + 5 printfn "Result=%i" b """ - let expected = { Start = pos 2 4; End = pos 2 7 } - text |> assertRangeIs expected - testCase "can extract range over multiple lines" <| fun _ -> - let text = """ + + let expected = { Start = pos 2 4; End = pos 2 7 } + text |> assertRangeIs expected + + testCase "can extract range over multiple lines" + <| fun _ -> + let text = + """ module Foo let $0a = 42 -let b = +let b = a + 5 printfn "$0Result=%i" b """ - let expected = { Start = pos 2 4; End = pos 5 9 } - text |> assertRangeIs expected - testCase "can extract position" <| fun _ -> - let text = """ + + let expected = { Start = pos 2 4; End = pos 5 9 } + text |> assertRangeIs expected + + testCase "can extract position" + <| fun _ -> + let text = + """ module Foo let a =$0 42 -let b = +let b = a + 5 printfn "Result=%i" b """ - let expected = { Start = pos 2 7; End = pos 2 7 } - text |> assertRangeIs expected - testCase "removes cursor markers from line" <| fun _ -> - let text = """ + + let expected = { Start = pos 2 7; End = pos 2 7 } + text |> assertRangeIs expected + + testCase "removes cursor markers from line" + <| fun _ -> + let text = + """ module Foo let $0a = 42 -let b = +let b = a + 5 printfn "$0Result=%i" b """ - let expected = !- """ + + let expected = + !- """ module Foo let a = 42 -let b = +let b = a + 5 printfn "Result=%i" b """ - text |> assertTextIs expected - testCase "finds and removes only first range and its two markers" <| fun _ -> - let text = """ + + text |> assertTextIs expected + + testCase "finds and removes only first range and its two markers" + <| fun _ -> + let text = + """ module $0Foo let a = $042 -let b = +let b = a + $05 printfn "$0Result$0=%i$0" b$0 """ - let expectedRange = { Start = pos 0 7; End = pos 2 8 } - let expectedText = !- """ + + let expectedRange = { Start = pos 0 7; End = pos 2 8 } + + let expectedText = + !- """ module Foo let a = 42 -let b = +let b = a + $05 printfn "$0Result$0=%i$0" b$0 """ - text |> assertResultIs (expectedRange, expectedText) - ] - - let beforeIndexTests = testList (nameof Cursor.beforeIndex) [ - let assertBeforeIndex expected textWithCursor = - let textWithCursor = Text.trimTripleQuotation textWithCursor - let idx = textWithCursor.IndexOf Cursor.Marker - Expect.isGreaterThanOrEqual "Text has no cursor" (idx, 0) - let text = textWithCursor.Remove(idx, Cursor.Marker.Length) - - text - |> Cursor.beforeIndex idx - |> Expect.equal "Should be correct position" expected - - - testList "single line" [ - testCase "empty string" <| fun _ -> - let text = "" - let idx = 0 - let expected = pos 0 0 - - text - |> Cursor.beforeIndex idx - |> Expect.equal "Position should be at start of string" expected - - testCase "empty string with cursor" <| fun _ -> - let text = "$0" - let expected = pos 0 0 - assertBeforeIndex expected text - - testCase "single line string - start" <| fun _ -> - let text = "$0let foo = 42" - let expected = pos 0 0 - assertBeforeIndex expected text - testCase "single line string - middle" <| fun _ -> - let text = "let foo $0= 42" - let expected = pos 0 8 - assertBeforeIndex expected text - testCase "single line string - end" <| fun _ -> - let text = "let foo = 42$0" - let expected = pos 0 12 - assertBeforeIndex expected text - ] - testList "multi line" [ - testCase "start of first line" <| fun _ -> - let text = """ + + text |> assertResultIs (expectedRange, expectedText) ] + + let beforeIndexTests = + testList + (nameof Cursor.beforeIndex) + [ let assertBeforeIndex expected textWithCursor = + let textWithCursor = Text.trimTripleQuotation textWithCursor + let idx = textWithCursor.IndexOf(Cursor.Marker, StringComparison.Ordinal) + Expect.isGreaterThanOrEqual "Text has no cursor" (idx, 0) + let text = textWithCursor.Remove(idx, Cursor.Marker.Length) + + text + |> Cursor.beforeIndex idx + |> Expect.equal "Should be correct position" expected + + + testList + "single line" + [ testCase "empty string" + <| fun _ -> + let text = "" + let idx = 0 + let expected = pos 0 0 + + text + |> Cursor.beforeIndex idx + |> Expect.equal "Position should be at start of string" expected + + testCase "empty string with cursor" + <| fun _ -> + let text = "$0" + let expected = pos 0 0 + assertBeforeIndex expected text + + testCase "single line string - start" + <| fun _ -> + let text = "$0let foo = 42" + let expected = pos 0 0 + assertBeforeIndex expected text + testCase "single line string - middle" + <| fun _ -> + let text = "let foo $0= 42" + let expected = pos 0 8 + assertBeforeIndex expected text + testCase "single line string - end" + <| fun _ -> + let text = "let foo = 42$0" + let expected = pos 0 12 + assertBeforeIndex expected text ] + + testList + "multi line" + [ testCase "start of first line" + <| fun _ -> + let text = + """ $0module Foo let a = 42 @@ -559,10 +711,13 @@ let b = a + 5 printfn "Result=%i" b """ - let expected = pos 0 0 - assertBeforeIndex expected text - testCase "middle of first line" <| fun _ -> - let text = """ + + let expected = pos 0 0 + assertBeforeIndex expected text + testCase "middle of first line" + <| fun _ -> + let text = + """ module $0Foo let a = 42 @@ -570,10 +725,13 @@ let b = a + 5 printfn "Result=%i" b """ - let expected = pos 0 7 - assertBeforeIndex expected text - testCase "end of first line" <| fun _ -> - let text = """ + + let expected = pos 0 7 + assertBeforeIndex expected text + testCase "end of first line" + <| fun _ -> + let text = + """ module Foo$0 let a = 42 @@ -581,10 +739,13 @@ let b = a + 5 printfn "Result=%i" b """ - let expected = pos 0 10 - assertBeforeIndex expected text - testCase "start of 4th line" <| fun _ -> - let text = """ + + let expected = pos 0 10 + assertBeforeIndex expected text + testCase "start of 4th line" + <| fun _ -> + let text = + """ module Foo let a = 42 @@ -592,10 +753,13 @@ $0let b = a + 5 printfn "Result=%i" b """ - let expected = pos 3 0 - assertBeforeIndex expected text - testCase "middle of 4th line" <| fun _ -> - let text = """ + + let expected = pos 3 0 + assertBeforeIndex expected text + testCase "middle of 4th line" + <| fun _ -> + let text = + """ module Foo let a = 42 @@ -603,10 +767,13 @@ let $0b = a + 5 printfn "Result=%i" b """ - let expected = pos 3 4 - assertBeforeIndex expected text - testCase "end of 4th line" <| fun _ -> - let text = """ + + let expected = pos 3 4 + assertBeforeIndex expected text + testCase "end of 4th line" + <| fun _ -> + let text = + """ module Foo let a = 42 @@ -614,213 +781,279 @@ let b =$0 a + 5 printfn "Result=%i" b """ - let expected = pos 3 7 - assertBeforeIndex expected text - testCase "start of last line" <| fun _ -> - let text = """ + + let expected = pos 3 7 + assertBeforeIndex expected text + testCase "start of last line" + <| fun _ -> + let text = + """ module Foo let a = 42 let b = a + 5 $0printfn "Result=%i" b""" - let expected = pos 5 0 - assertBeforeIndex expected text - testCase "middle of last line" <| fun _ -> - let text = """ + + let expected = pos 5 0 + assertBeforeIndex expected text + testCase "middle of last line" + <| fun _ -> + let text = + """ module Foo let a = 42 let b = a + 5 printfn "$0Result=%i" b""" - let expected = pos 5 9 - assertBeforeIndex expected text - testCase "end of last line" <| fun _ -> - let text = """ + + let expected = pos 5 9 + assertBeforeIndex expected text + testCase "end of last line" + <| fun _ -> + let text = + """ module Foo let a = 42 let b = a + 5 printfn "Result=%i" b$0""" - let expected = pos 5 21 - assertBeforeIndex expected text - ] - ] - - let tryIndexOfTests = testList (nameof Cursor.tryIndexOf) [ - let assertAndGetTextAndCursor = - Text.trimTripleQuotation - >> Cursor.tryExtractPosition - >> Option.defaultWith (fun _ -> failtest "should have found cursor") - let indexOf = - assertAndGetTextAndCursor - >> fun (pos, text) -> Cursor.tryIndexOf pos text - let assertAndGetIndexOf = - indexOf - >> function - | Ok i -> i - | Result.Error msg -> failtest $"should have found index. But was error: {msg}" - let assertIndexOf expectedIndex = - assertAndGetIndexOf - >> Expect.equal "wrong index" expectedIndex - let assertIndexOf expectedIndex textWithCursor = - let (pos, text) = assertAndGetTextAndCursor textWithCursor - match Cursor.tryIndexOf pos text with - | Ok actualIndex -> - let idxInText = textWithCursor.IndexOf Cursor.Marker - let errorMsg = $"wrong index. Cursor at Postion={{Line={pos.Line};Char={pos.Character}}} or Index={idxInText}" - Expect.equal errorMsg expectedIndex actualIndex - | Result.Error msg -> failtest $"should have found index. But was error: {msg}" - let assertNoIndexAt pos = - Text.trimTripleQuotation - >> Cursor.tryIndexOf pos - >> function - | Ok i -> failtest $"Expected Error, but was OK with index {i}" - | Result.Error _ -> () - - testList "empty string" [ - testCase "inside" <| fun _ -> - let text = "$0" - let expected = 0 - text |> assertIndexOf expected - testCase "out of char range in empty string" <| fun _ -> - let text = "" - let pos = pos 0 1 - text |> assertNoIndexAt pos - testCase "out of line range in empty string" <| fun _ -> - let text = "" - let pos = pos 1 0 - text |> assertNoIndexAt pos - ] - - testList "single line" [ - testCase "out of char range" <| fun _ -> - let text = "foo bar baz" - let pos = pos 0 (11 + 1) - text |> assertNoIndexAt pos - testCase "out of line range" <| fun _ -> - let text = "foo bar baz" - let pos = pos 1 0 - text |> assertNoIndexAt pos - testCase "start" <| fun _ -> - let text = "$0foo bar baz" - let expected = 0 - text |> assertIndexOf expected - testCase "middle" <| fun _ -> - let text = "foo b$0ar baz" - let expected = 5 - text |> assertIndexOf expected - testCase "end" <| fun _ -> - let text = "foo bar baz$0" - let expected = 11 - text |> assertIndexOf expected - ] - - testList "two lines" [ - testCase "start of 1st line" <| fun _ -> - // chars: 11 + `\n` + 17 - let text = "$0foo bar baz\nlorem ipsum dolor" - let expected = 0 - text |> assertIndexOf expected - testCase "middle of 1st line" <| fun _ -> - let text = "foo b$0ar baz\nlorem ipsum dolor" - let expected = 5 - text |> assertIndexOf expected - testCase "end of 1st line" <| fun _ -> - let text = "foo bar baz$0\nlorem ipsum dolor" - let expected = 10 (*1st line; 0-based*) + 1 (*\n*) // on `\n`; 10: Index is 0-based: string with length=11 -> max index = 10 - text |> assertIndexOf expected - testCase "start of 2nd line" <| fun _ -> - let text = "foo bar baz\n$0lorem ipsum dolor" - let expected = 10 (*1st line; 0-based*) + 1 (*\n*) + 0 (*2nd line*) + 1 (*index after cursor*) - text |> assertIndexOf expected - testCase "middle of 2nd line" <| fun _ -> - let text = "foo bar baz\nlorem ip$0sum dolor" - let expected = 10 (*1st line; 0-based*) + 1 (*\n*) + 8 (*2nd line*) + 1 (*index after cursor*) - text |> assertIndexOf expected - testCase "end of 2nd line" <| fun _ -> - let text = "foo bar baz\nlorem ipsum dolor$0" - let expected = 10 (*1st line; 0-based*) + 1 (*\n*) + 17 (*2nd line*) + 1 (*index afrer cursor*) - text |> assertIndexOf expected - testCase "out of char range in 1st line" <| fun _ -> - let text = "foo bar baz\nlorem ipsum dolor" - let pos = pos 0 (11 + 1) - text |> assertNoIndexAt pos - testCase "out of char range in 2nd line" <| fun _ -> - let text = "foo bar baz\nlorem ipsum dolor" - let pos = pos 1 (17 + 1) - text |> assertNoIndexAt pos - testCase "out of line range" <| fun _ -> - let text = "foo bar baz\nlorem ipsum dolor" - let pos = pos 2 0 - text |> assertNoIndexAt pos - ] - - testList "F# code" [ - testCase "start of text" <| fun _ -> - let text = """ + + let expected = pos 5 21 + assertBeforeIndex expected text ] ] + + let tryIndexOfTests = + testList + (nameof Cursor.tryIndexOf) + [ let assertAndGetTextAndCursor = + Text.trimTripleQuotation + >> Cursor.tryExtractPosition + >> Option.defaultWith (fun _ -> failtest "should have found cursor") + + let indexOf = + assertAndGetTextAndCursor >> fun (pos, text) -> Cursor.tryIndexOf pos text + + let assertAndGetIndexOf = + indexOf + >> function + | Ok i -> i + | Result.Error msg -> failtest $"should have found index. But was error: {msg}" + + let _assertIndexOf expectedIndex = assertAndGetIndexOf >> Expect.equal "wrong index" expectedIndex + + let assertIndexOf expectedIndex textWithCursor = + let (pos, text) = assertAndGetTextAndCursor textWithCursor + + match Cursor.tryIndexOf pos text with + | Ok actualIndex -> + let idxInText = textWithCursor.IndexOf(Cursor.Marker, StringComparison.Ordinal) + + let errorMsg = + $"wrong index. Cursor at Postion={{Line={pos.Line};Char={pos.Character}}} or Index={idxInText}" + + Expect.equal errorMsg expectedIndex actualIndex + | Result.Error msg -> failtest $"should have found index. But was error: {msg}" + + let assertNoIndexAt pos = + Text.trimTripleQuotation + >> Cursor.tryIndexOf pos + >> function + | Ok i -> failtest $"Expected Error, but was OK with index {i}" + | Result.Error _ -> () + + testList + "empty string" + [ testCase "inside" + <| fun _ -> + let text = "$0" + let expected = 0 + text |> assertIndexOf expected + testCase "out of char range in empty string" + <| fun _ -> + let text = "" + let pos = pos 0 1 + text |> assertNoIndexAt pos + testCase "out of line range in empty string" + <| fun _ -> + let text = "" + let pos = pos 1 0 + text |> assertNoIndexAt pos ] + + testList + "single line" + [ testCase "out of char range" + <| fun _ -> + let text = "foo bar baz" + let pos = pos 0 (11 + 1) + text |> assertNoIndexAt pos + testCase "out of line range" + <| fun _ -> + let text = "foo bar baz" + let pos = pos 1 0 + text |> assertNoIndexAt pos + testCase "start" + <| fun _ -> + let text = "$0foo bar baz" + let expected = 0 + text |> assertIndexOf expected + testCase "middle" + <| fun _ -> + let text = "foo b$0ar baz" + let expected = 5 + text |> assertIndexOf expected + testCase "end" + <| fun _ -> + let text = "foo bar baz$0" + let expected = 11 + text |> assertIndexOf expected ] + + testList + "two lines" + [ testCase "start of 1st line" + <| fun _ -> + // chars: 11 + `\n` + 17 + let text = "$0foo bar baz\nlorem ipsum dolor" + let expected = 0 + text |> assertIndexOf expected + testCase "middle of 1st line" + <| fun _ -> + let text = "foo b$0ar baz\nlorem ipsum dolor" + let expected = 5 + text |> assertIndexOf expected + testCase "end of 1st line" + <| fun _ -> + let text = "foo bar baz$0\nlorem ipsum dolor" + let expected = 10 (*1st line; 0-based*) + 1 (*\n*) // on `\n`; 10: Index is 0-based: string with length=11 -> max index = 10 + text |> assertIndexOf expected + testCase "start of 2nd line" + <| fun _ -> + let text = "foo bar baz\n$0lorem ipsum dolor" + + let expected = + 10 (*1st line; 0-based*) + 1 (*\n*) + 0 (*2nd line*) + 1 (*index after cursor*) + + text |> assertIndexOf expected + testCase "middle of 2nd line" + <| fun _ -> + let text = "foo bar baz\nlorem ip$0sum dolor" + + let expected = + 10 (*1st line; 0-based*) + 1 (*\n*) + 8 (*2nd line*) + 1 (*index after cursor*) + + text |> assertIndexOf expected + testCase "end of 2nd line" + <| fun _ -> + let text = "foo bar baz\nlorem ipsum dolor$0" + + let expected = + 10 (*1st line; 0-based*) + + 1 (*\n*) + + 17 (*2nd line*) + + 1 (*index afrer cursor*) + + text |> assertIndexOf expected + testCase "out of char range in 1st line" + <| fun _ -> + let text = "foo bar baz\nlorem ipsum dolor" + let pos = pos 0 (11 + 1) + text |> assertNoIndexAt pos + testCase "out of char range in 2nd line" + <| fun _ -> + let text = "foo bar baz\nlorem ipsum dolor" + let pos = pos 1 (17 + 1) + text |> assertNoIndexAt pos + testCase "out of line range" + <| fun _ -> + let text = "foo bar baz\nlorem ipsum dolor" + let pos = pos 2 0 + text |> assertNoIndexAt pos ] + + testList + "F# code" + [ testCase "start of text" + <| fun _ -> + let text = + """ $0module Foo let a = 42 -let b = +let b = a + 5 printfn "Result=%i" b """ - text |> assertIndexOf 0 - testCase "end of text" <| fun _ -> - let text = """ + + text |> assertIndexOf 0 + testCase "end of text" + <| fun _ -> + let text = + """ module Foo let a = 42 -let b = +let b = a + 5 printfn "Result=%i" b $0""" - text |> assertIndexOf 62 - testCase "middle of 1st line" <| fun _ -> - let text = """ + + text |> assertIndexOf 61 + testCase "middle of 1st line" + <| fun _ -> + let text = + """ module$0 Foo let a = 42 -let b = +let b = a + 5 printfn "Result=%i" b """ - text |> assertIndexOf 6 - testCase "end of 1st line" <| fun _ -> - let text = """ + + text |> assertIndexOf 6 + testCase "end of 1st line" + <| fun _ -> + let text = + """ module Foo$0 let a = 42 -let b = +let b = a + 5 printfn "Result=%i" b """ - text |> assertIndexOf 10 - testCase "start of 4th line" <| fun _ -> - let text = """ + + text |> assertIndexOf 10 + testCase "start of 4th line" + <| fun _ -> + let text = + """ module Foo let a = 42 -$0let b = +$0let b = a + 5 printfn "Result=%i" b """ - text |> assertIndexOf 23 - testCase "middle of 4th line" <| fun _ -> - let text = """ + + text |> assertIndexOf 23 + testCase "middle of 4th line" + <| fun _ -> + let text = + """ module Foo let a = 42 -let $0b = +let $0b = a + 5 printfn "Result=%i" b """ - text |> assertIndexOf 27 - testCase "end of 4th line" <| fun _ -> - let text = """ + + text |> assertIndexOf 27 + testCase "end of 4th line" + <| fun _ -> + let text = + """ module Foo let a = 42 @@ -828,57 +1061,70 @@ let b = $0 a + 5 printfn "Result=%i" b """ - text |> assertIndexOf 31 - ] - ] - - let identityTests = testList "identities" [ - // * idx |> beforeIndex |> tryIndexOf = idx - // * pos |> tryIndexOf |> beforeIndex = pos - // * tryExtractIndex >> beforeIndex = tryExtractPosition - // * tryExtractPosition >> tryIndexOf = tryExtractIndex - - // assert: `lhs = rhs` - let assertEquality lhs rhs textWithCursor = - let textWithCursor = textWithCursor |> Text.trimTripleQuotation - let lhs = textWithCursor |> lhs - let rhs = textWithCursor |> rhs - Expect.equal "Should hold: lhs = rhs (expected = actual)" lhs rhs - let testEquality lhs rhs name textWithCursor = - testCase name <| fun _ -> assertEquality lhs rhs textWithCursor - let testEqualityForAllCursors lhs rhs textWithCursors = - textWithCursors - |> Text.trimTripleQuotation - |> Cursors.iter - |> List.mapi (fun i t -> testEquality lhs rhs $"Cursor {i}" t) - - /// assert: `value |> roundTrip = value` - let assertThereAndBackAgain value roundTrip textWithCursor = - let textWithCursor = textWithCursor |> Text.trimTripleQuotation - let (value,text) = textWithCursor |> value - let roundTripped = (value, text) ||> roundTrip - Expect.equal "Should hold: value |> roundTrip = value (expected |> roundTrip = actual)" value roundTripped - let testThereAndBackAgain value roundTrip name textWithCursor = - testCase name <| fun _ -> assertThereAndBackAgain value roundTrip textWithCursor - let testThereAndBackAgainForAllCursors value roundTrip textWithCursors = - textWithCursors - |> Text.trimTripleQuotation - |> Cursors.iter - |> List.mapi (fun i -> testThereAndBackAgain value roundTrip $"Cursor {i}") - - testList "idx |> beforeIndex |> tryIndexOf = idx" [ - let value (textWithCursor: string) = - let idx = textWithCursor.IndexOf Cursor.Marker - if idx < 0 then - failtest "No cursor" - let text = textWithCursor.Replace(Cursor.Marker, "") - (idx, text) - let roundTrip idx text = - let pos = Cursor.beforeIndex idx text - Cursor.tryIndexOf pos text - |> Result.defaultWith (fun error -> failtest $"Error while IndexOf: {error}") - testList "F# Code 1" ( - let text = """ + + text |> assertIndexOf 31 ] ] + + let identityTests = + testList + "identities" + [ + // * idx |> beforeIndex |> tryIndexOf = idx + // * pos |> tryIndexOf |> beforeIndex = pos + // * tryExtractIndex >> beforeIndex = tryExtractPosition + // * tryExtractPosition >> tryIndexOf = tryExtractIndex + + // assert: `lhs = rhs` + let assertEquality lhs rhs textWithCursor = + let textWithCursor = textWithCursor |> Text.trimTripleQuotation + let lhs = textWithCursor |> lhs + let rhs = textWithCursor |> rhs + Expect.equal "Should hold: lhs = rhs (expected = actual)" lhs rhs + + let testEquality lhs rhs name textWithCursor = testCase name <| fun _ -> assertEquality lhs rhs textWithCursor + + let testEqualityForAllCursors lhs rhs textWithCursors = + textWithCursors + |> Text.trimTripleQuotation + |> Cursors.iter + |> List.mapi (fun i t -> testEquality lhs rhs $"Cursor {i}" t) + + /// assert: `value |> roundTrip = value` + let assertThereAndBackAgain value roundTrip textWithCursor = + let textWithCursor = textWithCursor |> Text.trimTripleQuotation + let (value, text) = textWithCursor |> value + let roundTripped = (value, text) ||> roundTrip + Expect.equal "Should hold: value |> roundTrip = value (expected |> roundTrip = actual)" value roundTripped + + let testThereAndBackAgain value roundTrip name textWithCursor = + testCase name <| fun _ -> assertThereAndBackAgain value roundTrip textWithCursor + + let testThereAndBackAgainForAllCursors value roundTrip textWithCursors = + textWithCursors + |> Text.trimTripleQuotation + |> Cursors.iter + |> List.mapi (fun i -> testThereAndBackAgain value roundTrip $"Cursor {i}") + + testList + "idx |> beforeIndex |> tryIndexOf = idx" + [ let value (textWithCursor: string) = + let idx = textWithCursor.IndexOf(Cursor.Marker, StringComparison.Ordinal) + + if idx < 0 then + failtest "No cursor" + + let text = textWithCursor.Replace(Cursor.Marker, "") + (idx, text) + + let roundTrip idx text = + let pos = Cursor.beforeIndex idx text + + Cursor.tryIndexOf pos text + |> Result.defaultWith (fun error -> failtest $"Error while IndexOf: {error}") + + testList + "F# Code 1" + (let text = + """ $0module$0 Foo$0 $0let $0a = 42$0 @@ -886,22 +1132,28 @@ let b = $0a $0+ 5$0 $0printfn "$0Result=%i" b$0 """ - text |> testThereAndBackAgainForAllCursors value roundTrip - ) - ] - testList "pos |> tryIndexOf |> beforeIndex = pos" [ - let value (textWithCursor: string) = - textWithCursor - |> Cursor.tryExtractPosition - |> Option.defaultWith (fun _ -> failtest "No cursor") - let roundTrip pos text = - let idx = - text - |> Cursor.tryIndexOf pos - |> Result.defaultWith (fun error -> failtest $"Error while IndexOf: {error}") - Cursor.beforeIndex idx text - testList "F# Code 1" ( - let text = """ + + text |> testThereAndBackAgainForAllCursors value roundTrip) ] + + testList + "pos |> tryIndexOf |> beforeIndex = pos" + [ let value (textWithCursor: string) = + textWithCursor + |> Cursor.tryExtractPosition + |> Option.defaultWith (fun _ -> failtest "No cursor") + + let roundTrip pos text = + let idx = + text + |> Cursor.tryIndexOf pos + |> Result.defaultWith (fun error -> failtest $"Error while IndexOf: {error}") + + Cursor.beforeIndex idx text + + testList + "F# Code 1" + (let text = + """ $0module$0 Foo$0 $0let $0a = 42$0 @@ -909,20 +1161,25 @@ let b = $0a $0+ 5$0 $0printfn "$0Result=%i" b$0 """ - text |> testThereAndBackAgainForAllCursors value roundTrip - ) - ] - testList "tryExtractIndex >> beforeIndex = tryExtractPosition" [ - let lhs = - Cursor.tryExtractIndex - >> Option.defaultWith (fun _ -> failtest "No cursor") - >> fun (idx, text) -> Cursor.beforeIndex idx text - let rhs = - Cursor.tryExtractPosition - >> Option.defaultWith (fun _ -> failtest "No cursor") - >> fst - testList "F# Code 1" ( - let text = """ + + text |> testThereAndBackAgainForAllCursors value roundTrip) ] + + testList + "tryExtractIndex >> beforeIndex = tryExtractPosition" + [ let lhs = + Cursor.tryExtractIndex + >> Option.defaultWith (fun _ -> failtest "No cursor") + >> fun (idx, text) -> Cursor.beforeIndex idx text + + let rhs = + Cursor.tryExtractPosition + >> Option.defaultWith (fun _ -> failtest "No cursor") + >> fst + + testList + "F# Code 1" + (let text = + """ $0module$0 Foo$0 $0let $0a = 42$0 @@ -930,21 +1187,26 @@ let b = $0a $0+ 5$0 $0printfn "$0Result=%i" b$0 """ - text |> testEqualityForAllCursors lhs rhs - ) - ] - testList "tryExtractPosition >> tryIndexOf = tryExtractIndex" [ - let lhs = - Cursor.tryExtractPosition - >> Option.defaultWith (fun _ -> failtest "No cursor") - >> fun (pos, text) -> Cursor.tryIndexOf pos text - >> Result.defaultWith (fun error -> failtest $"No index: {error}") - let rhs = - Cursor.tryExtractIndex - >> Option.defaultWith (fun _ -> failtest "No cursor") - >> fst - testList "F# Code 1" ( - let text = """ + + text |> testEqualityForAllCursors lhs rhs) ] + + testList + "tryExtractPosition >> tryIndexOf = tryExtractIndex" + [ let lhs = + Cursor.tryExtractPosition + >> Option.defaultWith (fun _ -> failtest "No cursor") + >> fun (pos, text) -> Cursor.tryIndexOf pos text + >> Result.defaultWith (fun error -> failtest $"No index: {error}") + + let rhs = + Cursor.tryExtractIndex + >> Option.defaultWith (fun _ -> failtest "No cursor") + >> fst + + testList + "F# Code 1" + (let text = + """ $0module$0 Foo$0 $0let $0a = 42$0 @@ -952,213 +1214,218 @@ let b = $0a $0+ 5$0 $0printfn "$0Result=%i" b$0 """ - text |> testEqualityForAllCursors lhs rhs - ) - ] - ] - - let afterEditsTests = testList (nameof Cursor.afterEdits) [ - testCase "doesn't move cursor when insert after cursor in different line" <| fun _ -> - let cursor = pos 1 2 - let edits = [ - { - Range = posRange (pos 2 5) - NewText = "foo bar" - } - ] - cursor - |> Cursor.afterEdits edits - |> Expect.equal "Cursor should not move" cursor - testCase "doesn't move cursor when remove after cursor in different line" <| fun _ -> - let cursor = pos 1 2 - let edits = [ - { - Range = range (pos 2 5) (pos 3 4) - NewText = "" - } - ] - cursor - |> Cursor.afterEdits edits - |> Expect.equal "Cursor should not move" cursor - testCase "doesn't move cursor when replace after cursor in different line" <| fun _ -> - let cursor = pos 1 2 - let edits = [ - { - Range = range (pos 2 5) (pos 3 4) - NewText = "foo bar" - } - ] - cursor - |> Cursor.afterEdits edits - |> Expect.equal "Cursor should not move" cursor - - testCase "doesn't move cursor when insert before cursor in different line and just inside line" <| fun _ -> - let cursor = pos 2 2 - let edits = [ - { - Range = posRange (pos 1 5) - NewText = "foo bar" - } - ] - cursor - |> Cursor.afterEdits edits - |> Expect.equal "Cursor should not move" cursor - testCase "doesn't move cursor when remove before cursor in different line and just inside line" <| fun _ -> - let cursor = pos 2 2 - let edits = [ - { - Range = range (pos 1 5) (pos 1 7) - NewText = "" - } - ] - cursor - |> Cursor.afterEdits edits - |> Expect.equal "Cursor should not move" cursor - testCase "doesn't move cursor when replace before cursor in different line and just inside line" <| fun _ -> - let cursor = pos 2 2 - let edits = [ - { - Range = range (pos 1 5) (pos 1 7) - NewText = "foo bar" - } - ] - cursor - |> Cursor.afterEdits edits - |> Expect.equal "Cursor should not move" cursor - - testCase "moves cursor down a line when inserting new line before cursor in different line" <| fun _ -> - let cursor = pos 2 2 - let edits = [ - { - Range = posRange (pos 1 5) - NewText = "foo\nbar" - } - ] - cursor - |> Cursor.afterEdits edits - |> Expect.equal "Cursor should move down a line" { cursor with Line = cursor.Line + 1 } - testCase "moves cursor up a line when removing line before cursor in different line" <| fun _ -> - let cursor = pos 3 2 - let edits = [ - { - Range = range (pos 1 5) (pos 2 4) - NewText = "" - } - ] - cursor - |> Cursor.afterEdits edits - |> Expect.equal "Cursor should move up a line" { cursor with Line = cursor.Line - 1 } - - testCase "moves cursor up a line when removing a line and inserting inside line before cursor in different line" <| fun _ -> - let cursor = pos 3 2 - let edits = [ - { - Range = range (pos 1 5) (pos 2 4) - NewText = "foo bar" - } - ] - cursor - |> Cursor.afterEdits edits - |> Expect.equal "Cursor should move up a line" { cursor with Line = cursor.Line - 1 } - testCase "doesn't move cursor when removing a line and inserting a line before cursor in different line" <| fun _ -> - let cursor = pos 3 2 - let edits = [ - { - Range = range (pos 1 5) (pos 2 4) - NewText = "foo\nbar" - } - ] - cursor - |> Cursor.afterEdits edits - |> Expect.equal "Cursor should not move" cursor - testCase "moves cursor down when removing a line and inserting two lines before cursor in different line" <| fun _ -> - let cursor = pos 3 2 - let edits = [ - { - Range = range (pos 1 5) (pos 2 4) - NewText = "foo\nbar\nbaz" - } - ] - cursor - |> Cursor.afterEdits edits - |> Expect.equal "Cursor should move down a line" { cursor with Line = cursor.Line + 1 } - - testCase "moves cursor back when inserting inside same line in front of cursor" <| fun _ -> - let cursor = pos 3 2 - let edits = [ - { - Range = posRange (pos 3 1) - NewText = "foo" - } - ] - cursor - |> Cursor.afterEdits edits - |> Expect.equal "Cursor should move back" { cursor with Character = cursor.Character + 3 } - testCase "moves cursor forward when deleting inside same line in front of cursor" <| fun _ -> - let cursor = pos 3 7 - let edits = [ - { - Range = range (pos 3 2) (pos 3 5) - NewText = "" - } - ] - cursor - |> Cursor.afterEdits edits - |> Expect.equal "Cursor should move forward" { cursor with Character = 4 } - - testCase "moves cursor forward and up when deleting inside and pre same line in front of cursor" <| fun _ -> - let cursor = pos 3 7 - let edits = [ - { - Range = range (pos 2 2) (pos 3 5) - NewText = "" - } - ] - cursor - |> Cursor.afterEdits edits - |> Expect.equal "Cursor should move forward and up" (pos 2 4) - - - testCase "moves cursor to front of delete when cursor inside" <| fun _ -> - let cursor = pos 3 7 - let edits = [ - { - Range = range (pos 2 2) (pos 3 10) - NewText = "" - } - ] - cursor - |> Cursor.afterEdits edits - |> Expect.equal "Cursor should move to start of delete" (pos 2 2) - - testCase "cursor stays when insert at cursor position" <| fun _ -> - let cursor = pos 2 5 - let edits = [ - { - Range = posRange (pos 2 5) - NewText = "foo bar" - } - ] - cursor - |> Cursor.afterEdits edits - |> Expect.equal "Cursor should move to start of delete" cursor - - testCase "cursor moves to front when replacement with cursor inside" <| fun _ -> - let cursor = pos 3 7 - let edits = [ - { - Range = range (pos 2 3) (pos 5 2) - NewText = "foo bar" - } - ] - cursor - |> Cursor.afterEdits edits - |> Expect.equal "Cursor should move to start of delete" { Line = 2; Character = 3 } - - testList "multiple edits" [ - let data = lazy ( - let textWithCursors = - """ + + text |> testEqualityForAllCursors lhs rhs) ] ] + + let afterEditsTests = + testList + (nameof Cursor.afterEdits) + [ testCase "doesn't move cursor when insert after cursor in different line" + <| fun _ -> + let cursor = pos 1 2 + + let edits = + [ { Range = posRange (pos 2 5) + NewText = "foo bar" } ] + + cursor + |> Cursor.afterEdits edits + |> Expect.equal "Cursor should not move" cursor + testCase "doesn't move cursor when remove after cursor in different line" + <| fun _ -> + let cursor = pos 1 2 + + let edits = + [ { Range = range (pos 2 5) (pos 3 4) + NewText = "" } ] + + cursor + |> Cursor.afterEdits edits + |> Expect.equal "Cursor should not move" cursor + testCase "doesn't move cursor when replace after cursor in different line" + <| fun _ -> + let cursor = pos 1 2 + + let edits = + [ { Range = range (pos 2 5) (pos 3 4) + NewText = "foo bar" } ] + + cursor + |> Cursor.afterEdits edits + |> Expect.equal "Cursor should not move" cursor + + testCase "doesn't move cursor when insert before cursor in different line and just inside line" + <| fun _ -> + let cursor = pos 2 2 + + let edits = + [ { Range = posRange (pos 1 5) + NewText = "foo bar" } ] + + cursor + |> Cursor.afterEdits edits + |> Expect.equal "Cursor should not move" cursor + testCase "doesn't move cursor when remove before cursor in different line and just inside line" + <| fun _ -> + let cursor = pos 2 2 + + let edits = + [ { Range = range (pos 1 5) (pos 1 7) + NewText = "" } ] + + cursor + |> Cursor.afterEdits edits + |> Expect.equal "Cursor should not move" cursor + testCase "doesn't move cursor when replace before cursor in different line and just inside line" + <| fun _ -> + let cursor = pos 2 2 + + let edits = + [ { Range = range (pos 1 5) (pos 1 7) + NewText = "foo bar" } ] + + cursor + |> Cursor.afterEdits edits + |> Expect.equal "Cursor should not move" cursor + + testCase "moves cursor down a line when inserting new line before cursor in different line" + <| fun _ -> + let cursor = pos 2 2 + + let edits = + [ { Range = posRange (pos 1 5) + NewText = "foo\nbar" } ] + + cursor + |> Cursor.afterEdits edits + |> Expect.equal "Cursor should move down a line" { cursor with Line = cursor.Line + 1 } + testCase "moves cursor up a line when removing line before cursor in different line" + <| fun _ -> + let cursor = pos 3 2 + + let edits = + [ { Range = range (pos 1 5) (pos 2 4) + NewText = "" } ] + + cursor + |> Cursor.afterEdits edits + |> Expect.equal "Cursor should move up a line" { cursor with Line = cursor.Line - 1 } + + testCase "moves cursor up a line when removing a line and inserting inside line before cursor in different line" + <| fun _ -> + let cursor = pos 3 2 + + let edits = + [ { Range = range (pos 1 5) (pos 2 4) + NewText = "foo bar" } ] + + cursor + |> Cursor.afterEdits edits + |> Expect.equal "Cursor should move up a line" { cursor with Line = cursor.Line - 1 } + testCase "doesn't move cursor when removing a line and inserting a line before cursor in different line" + <| fun _ -> + let cursor = pos 3 2 + + let edits = + [ { Range = range (pos 1 5) (pos 2 4) + NewText = "foo\nbar" } ] + + cursor + |> Cursor.afterEdits edits + |> Expect.equal "Cursor should not move" cursor + testCase "moves cursor down when removing a line and inserting two lines before cursor in different line" + <| fun _ -> + let cursor = pos 3 2 + + let edits = + [ { Range = range (pos 1 5) (pos 2 4) + NewText = "foo\nbar\nbaz" } ] + + cursor + |> Cursor.afterEdits edits + |> Expect.equal "Cursor should move down a line" { cursor with Line = cursor.Line + 1 } + + testCase "moves cursor back when inserting inside same line in front of cursor" + <| fun _ -> + let cursor = pos 3 2 + + let edits = + [ { Range = posRange (pos 3 1) + NewText = "foo" } ] + + cursor + |> Cursor.afterEdits edits + |> Expect.equal + "Cursor should move back" + { cursor with + Character = cursor.Character + 3 } + testCase "moves cursor forward when deleting inside same line in front of cursor" + <| fun _ -> + let cursor = pos 3 7 + + let edits = + [ { Range = range (pos 3 2) (pos 3 5) + NewText = "" } ] + + cursor + |> Cursor.afterEdits edits + |> Expect.equal "Cursor should move forward" { cursor with Character = 4 } + + testCase "moves cursor forward and up when deleting inside and pre same line in front of cursor" + <| fun _ -> + let cursor = pos 3 7 + + let edits = + [ { Range = range (pos 2 2) (pos 3 5) + NewText = "" } ] + + cursor + |> Cursor.afterEdits edits + |> Expect.equal "Cursor should move forward and up" (pos 2 4) + + + testCase "moves cursor to front of delete when cursor inside" + <| fun _ -> + let cursor = pos 3 7 + + let edits = + [ { Range = range (pos 2 2) (pos 3 10) + NewText = "" } ] + + cursor + |> Cursor.afterEdits edits + |> Expect.equal "Cursor should move to start of delete" (pos 2 2) + + testCase "cursor stays when insert at cursor position" + <| fun _ -> + let cursor = pos 2 5 + + let edits = + [ { Range = posRange (pos 2 5) + NewText = "foo bar" } ] + + cursor + |> Cursor.afterEdits edits + |> Expect.equal "Cursor should move to start of delete" cursor + + testCase "cursor moves to front when replacement with cursor inside" + <| fun _ -> + let cursor = pos 3 7 + + let edits = + [ { Range = range (pos 2 3) (pos 5 2) + NewText = "foo bar" } ] + + cursor + |> Cursor.afterEdits edits + |> Expect.equal "Cursor should move to start of delete" { Line = 2; Character = 3 } + + testList + "multiple edits" + [ let data = + lazy + (let textWithCursors = + """ let $1foo$1 = 42 let $2bar = "baz" @@ -1169,355 +1436,327 @@ $0printfn "$0Result=%i" b$0 let $7res$7 = f (incr 4) (decr 3) """ - // match res with - // | _ when res < 0 -> failwith "negative" - // | 0 -> None - // | i -> Some i - // """ - |> Text.trimTripleQuotation - let edits = [ - // doesn't change cursor - {| - Marker = "$1" - NewText = "barbaz" - |} - // - 2 lines + 1 line - {| - Marker = "$2" - NewText = "baz = 42\nlet " - |} - // -1 line + 2 lines - {| - Marker = "$3" - NewText = "c\n b =\n (a+c-" - |} - // -1 line - 3 chars - {| - Marker = "$4" - NewText = "" - |} - // +3 line -all chars + couple new chars - {| - Marker = "$5" - NewText = " static\n\n\n mutable" - |} - // move to front of edit chars - {| - Marker = "$6" - NewText = "incrementNumber" - |} - // doesn't change cursor - {| - Marker = "$7" - NewText = "foo bar\nbaz\nlorem ipsum" - |} - ] - let markers = - edits - |> List.map (fun e -> e.Marker) - |> List.append ["$0"] - |> List.toArray - - let (text, cursors) = - textWithCursors - |> Cursors.extractGroupedWith markers - let cursor = cursors["$0"] |> List.head - - let edits = - edits - |> List.map (fun e -> - let range = - match cursors[e.Marker] with - | [s;e] -> range s e - | [pos] -> posRange pos - | cs -> failwith $"invalid number of cursors `{e.Marker}`. Expected 1 or 2, but was {cs.Length}" - { - Range = range - NewText = e.NewText - } - ) - |> TextEdits.sortByRange - - {| - Text = text - Cursor = cursor - Edits = edits - |} - ) - - testCase "cursor moves according to multiple edits" <| fun _ -> - let data = data.Value - let (text, cursor, edits) = data.Text, data.Cursor, data.Edits - - let textAfterEdits = - text - |> TextEdits.apply edits - |> Expect.wantOk "Edits should be valid" - // cursor should be at start of `incrementNumber` - let expected = - textAfterEdits - |> Text.lines - |> Seq.indexed - |> Seq.choose (fun (l, line) -> - match line.IndexOf "incrementNumber" with - | -1 -> None - | c -> Some (pos l c) - ) - |> Seq.exactlyOne - - cursor - |> Cursor.afterEdits edits - |> Expect.equal "Cursor should move according to edits" expected - - testCase "moving cursor for all edits together is same as moving cursor for each edit" <| fun _ -> - let data = data.Value - let (cursor, edits) = data.Cursor, data.Edits - - let individually = - edits - |> List.rev - |> List.fold (fun cursor edit -> - cursor |> Cursor.afterEdits [edit] - ) cursor - let together = + // match res with + // | _ when res < 0 -> failwith "negative" + // | 0 -> None + // | i -> Some i + // """ + |> Text.trimTripleQuotation + + let edits = + [ + // doesn't change cursor + {| Marker = "$1"; NewText = "barbaz" |} + // - 2 lines + 1 line + {| Marker = "$2" + NewText = "baz = 42\nlet " |} + // -1 line + 2 lines + {| Marker = "$3" + NewText = "c\n b =\n (a+c-" |} + // -1 line - 3 chars + {| Marker = "$4"; NewText = "" |} + // +3 line -all chars + couple new chars + {| Marker = "$5" + NewText = " static\n\n\n mutable" |} + // move to front of edit chars + {| Marker = "$6" + NewText = "incrementNumber" |} + // doesn't change cursor + {| Marker = "$7" + NewText = "foo bar\nbaz\nlorem ipsum" |} ] + + let markers = + edits |> List.map (fun e -> e.Marker) |> List.append [ "$0" ] |> List.toArray + + let (text, cursors) = textWithCursors |> Cursors.extractGroupedWith markers + let cursor = cursors["$0"] |> List.head + + let edits = + edits + |> List.map (fun e -> + let range = + match cursors[e.Marker] with + | [ s; e ] -> range s e + | [ pos ] -> posRange pos + | cs -> failwith $"invalid number of cursors `{e.Marker}`. Expected 1 or 2, but was {cs.Length}" + + { Range = range; NewText = e.NewText }) + |> TextEdits.sortByRange + + {| Text = text + Cursor = cursor + Edits = edits |}) + + testCase "cursor moves according to multiple edits" + <| fun _ -> + let data = data.Value + let (text, cursor, edits) = data.Text, data.Cursor, data.Edits + + let textAfterEdits = + text |> TextEdits.apply edits |> Expect.wantOk "Edits should be valid" + // cursor should be at start of `incrementNumber` + let expected = + textAfterEdits + |> Text.lines + |> Seq.indexed + |> Seq.choose (fun (l, line) -> + match line.IndexOf("incrementNumber", StringComparison.Ordinal) with + | -1 -> None + | c -> Some(pos l c)) + |> Seq.exactlyOne + + cursor + |> Cursor.afterEdits edits + |> Expect.equal "Cursor should move according to edits" expected + + testCase "moving cursor for all edits together is same as moving cursor for each edit" + <| fun _ -> + let data = data.Value + let (cursor, edits) = data.Cursor, data.Edits + + let individually = + edits + |> List.rev + |> List.fold (fun cursor edit -> cursor |> Cursor.afterEdits [ edit ]) cursor + + let together = cursor |> Cursor.afterEdits edits + + Expecto.Expect.equal + together + individually + "Moving cursor for all edits together should be same as moving cursor for each edit" ] + + testCase "Can add type annotation with parens while cursor stays at end of identifier" + <| fun _ -> + // `let foo$0 = 42` + let cursor = pos 0 7 + + let edits = + [ { Range = posRange (pos 0 4) + NewText = "(" } + { Range = posRange (pos 0 7) + NewText = ": int" } + { Range = posRange (pos 0 7) + NewText = ")" } ] + cursor |> Cursor.afterEdits edits - - Expecto.Expect.equal together individually "Moving cursor for all edits together should be same as moving cursor for each edit" - ] - - testCase "Can add type annotation with parens while cursor stays at end of identifier" <| fun _ -> - // `let foo$0 = 42` - let cursor = pos 0 7 - let edits = [ - { - Range = posRange (pos 0 4) - NewText = "(" - } - { - Range = posRange (pos 0 7) - NewText = ": int" - } - { - Range = posRange (pos 0 7) - NewText = ")" - } - ] - cursor - |> Cursor.afterEdits edits - |> Expect.equal "Cursor should move to end of identifier" { cursor with Character = cursor.Character + 1 } - ] - - let tests = testList (nameof Cursor) [ - tryExtractIndexTests - tryExtractPositionMarkedWithAnyOfTests - tryExtractPositionTests - tryExtractRangeTests - beforeIndexTests - tryIndexOfTests - identityTests - afterEditsTests - ] + |> Expect.equal + "Cursor should move to end of identifier" + { cursor with + Character = cursor.Character + 1 } ] + + let tests = + testList + (nameof Cursor) + [ tryExtractIndexTests + tryExtractPositionMarkedWithAnyOfTests + tryExtractPositionTests + tryExtractRangeTests + beforeIndexTests + tryIndexOfTests + identityTests + afterEditsTests ] module private Cursors = open Expecto.Flip - let private iterTests = testList (nameof Cursors.iter) [ - testCase "no cursor" <| fun _ -> - let text = "foo bar baz" - let expected = [] - text - |> Cursors.iter - |> Expect.equal "should be empty because no cursors" expected - testCase "one cursor" <| fun _ -> - let text = "foo $0bar baz" - let expected = [text] - text - |> Cursors.iter - |> Expect.equal "should have returned one strings with cursor" expected - testCase "two cursors" <| fun _ -> - let text = "foo $0bar baz$0" - let expected = [ - "foo $0bar baz" - "foo bar baz$0" - ] - text - |> Cursors.iter - |> Expect.equal "should have returned two strings with cursor" expected - testCase "three cursors" <| fun _ -> - let text = "$0foo $0bar baz$0" - let expected = [ - "$0foo bar baz" - "foo $0bar baz" - "foo bar baz$0" - ] - text - |> Cursors.iter - |> Expect.equal "should have returned three strings with cursor" expected - testCase "four cursors" <| fun _ -> - let text = "$0foo $0ba$0r baz$0" - let expected = [ - "$0foo bar baz" - "foo $0bar baz" - "foo ba$0r baz" - "foo bar baz$0" - ] - text - |> Cursors.iter - |> Expect.equal "should have returned three strings with cursor" expected - testCase "cursors in triple quoted string" <| fun _ -> - let text = !- """ + let private iterTests = + testList + (nameof Cursors.iter) + [ testCase "no cursor" + <| fun _ -> + let text = "foo bar baz" + let expected = [] + + text + |> Cursors.iter + |> Expect.equal "should be empty because no cursors" expected + testCase "one cursor" + <| fun _ -> + let text = "foo $0bar baz" + let expected = [ text ] + + text + |> Cursors.iter + |> Expect.equal "should have returned one strings with cursor" expected + testCase "two cursors" + <| fun _ -> + let text = "foo $0bar baz$0" + let expected = [ "foo $0bar baz"; "foo bar baz$0" ] + + text + |> Cursors.iter + |> Expect.equal "should have returned two strings with cursor" expected + testCase "three cursors" + <| fun _ -> + let text = "$0foo $0bar baz$0" + let expected = [ "$0foo bar baz"; "foo $0bar baz"; "foo bar baz$0" ] + + text + |> Cursors.iter + |> Expect.equal "should have returned three strings with cursor" expected + testCase "four cursors" + <| fun _ -> + let text = "$0foo $0ba$0r baz$0" + + let expected = + [ "$0foo bar baz"; "foo $0bar baz"; "foo ba$0r baz"; "foo bar baz$0" ] + + text + |> Cursors.iter + |> Expect.equal "should have returned three strings with cursor" expected + testCase "cursors in triple quoted string" + <| fun _ -> + let text = + !- """ module $0Foo let a = 42 -$0let b = +$0let b = a + 5$0 printfn "Result=%i$0" b$0 """ - let expected = [ - !- """ + + let expected = + [ !- """ module $0Foo let a = 42 -let b = +let b = a + 5 printfn "Result=%i" b """ - !- """ + !- """ module Foo let a = 42 -$0let b = +$0let b = a + 5 printfn "Result=%i" b """ - !- """ + !- """ module Foo let a = 42 -let b = +let b = a + 5$0 printfn "Result=%i" b """ - !- """ + !- """ module Foo let a = 42 -let b = +let b = a + 5 printfn "Result=%i$0" b """ - !- """ + !- """ module Foo let a = 42 -let b = +let b = a + 5 printfn "Result=%i" b$0 - """ - ] - text - |> Cursors.iter - |> Expect.equal "should have returned all strings with single cursor" expected - ] - - let private extractWithTests = testList (nameof Cursors.extractWith) [ - testCase "can extract all cursors" <| fun _ -> - let text = !- """ + """ ] + + text + |> Cursors.iter + |> Expect.equal "should have returned all strings with single cursor" expected ] + + let private extractWithTests = + testList + (nameof Cursors.extractWith) + [ testCase "can extract all cursors" + <| fun _ -> + let text = + !- """ let $Ff a b = a + b let $Vvalue = 42 let $0res = $Ff $Vvalue 3 () """ - let actual = - text - |> Cursors.extractWith [|"$F"; "$V"; "$0" |] - let expectedText = !- """ + let actual = text |> Cursors.extractWith [| "$F"; "$V"; "$0" |] + + let expectedText = + !- """ let f a b = a + b let value = 42 let res = f value 3 () """ - let expectedPoss = [ - ("$F", pos 0 4) - ("$V", pos 1 4) - ("$0", pos 2 4) - ("$F", pos 2 10) - ("$V", pos 2 12) - ] - let expected = (expectedText, expectedPoss) - - actual - |> Expect.equal "markers should match" expected - ] - - let tests = testList (nameof Cursors) [ - iterTests - extractWithTests - ] + + let expectedPoss = + [ ("$F", pos 0 4) + ("$V", pos 1 4) + ("$0", pos 2 4) + ("$F", pos 2 10) + ("$V", pos 2 12) ] + + let expected = (expectedText, expectedPoss) + + actual |> Expect.equal "markers should match" expected ] + + let tests = testList (nameof Cursors) [ iterTests; extractWithTests ] module private Text = open Expecto.Flip - let private removeTests = testList (nameof Text.remove) [ - testList "start=end should remove nothing" [ - let assertNothingChanged textWithCursor = - let (range, text) = - textWithCursor - |> Text.trimTripleQuotation - |> Cursor.tryExtractRange - |> Option.defaultWith (fun _ -> failtest $"no cursor found") - let expected = Ok text - text - |> Text.remove range - |> Expect.equal "shouldn't have changed input string" expected - - let just pos = range pos pos - testCase "empty string" <| fun _ -> - let text = "" - let range = just <| pos 0 0 - let expected = "" - text - |> Text.remove range - |> Expect.equal "shouldn't have change input string" (Ok expected) - testCase "empty string with two cursors" <| fun _ -> - "$0$0" - |> assertNothingChanged - testList "single line string" [ - testCase "start" <| fun _ -> - "$0foo bar baz" - |> assertNothingChanged - testCase "middle" <| fun _ -> - "foo b$0ar baz" - |> assertNothingChanged - testCase "end" <| fun _ -> - "foo bar baz$0" - |> assertNothingChanged - testCase "two cursors in middle" <| fun _ -> - "foo $0$0bar baz" - |> assertNothingChanged - ] - testList "two line string" [ - testCase "start" <| fun _ -> - "$0foo bar\n baz" - |> assertNothingChanged - testCase "end 1st line" <| fun _ -> - "foo bar$0\n baz" - |> assertNothingChanged - testCase "start 2nd line" <| fun _ -> - "foo bar\n$0 baz" - |> assertNothingChanged - testCase "middle 2nd line" <| fun _ -> - "foo bar\n ba$0z" - |> assertNothingChanged - testCase "end" <| fun _ -> - "foo bar\n baz$0" - |> assertNothingChanged - ] - testList "F# Code" ( - let text = """ + let private removeTests = + testList + (nameof Text.remove) + [ testList + "start=end should remove nothing" + [ let assertNothingChanged textWithCursor = + let (range, text) = + textWithCursor + |> Text.trimTripleQuotation + |> Cursor.tryExtractRange + |> Option.defaultWith (fun _ -> failtest $"no cursor found") + + let expected = Ok text + + text + |> Text.remove range + |> Expect.equal "shouldn't have changed input string" expected + + let just pos = range pos pos + + testCase "empty string" + <| fun _ -> + let text = "" + let range = just <| pos 0 0 + let expected = "" + + text + |> Text.remove range + |> Expect.equal "shouldn't have change input string" (Ok expected) + + testCase "empty string with two cursors" + <| fun _ -> "$0$0" |> assertNothingChanged + + testList + "single line string" + [ testCase "start" <| fun _ -> "$0foo bar baz" |> assertNothingChanged + testCase "middle" <| fun _ -> "foo b$0ar baz" |> assertNothingChanged + testCase "end" <| fun _ -> "foo bar baz$0" |> assertNothingChanged + testCase "two cursors in middle" + <| fun _ -> "foo $0$0bar baz" |> assertNothingChanged ] + + testList + "two line string" + [ testCase "start" <| fun _ -> "$0foo bar\n baz" |> assertNothingChanged + testCase "end 1st line" <| fun _ -> "foo bar$0\n baz" |> assertNothingChanged + testCase "start 2nd line" <| fun _ -> "foo bar\n$0 baz" |> assertNothingChanged + testCase "middle 2nd line" <| fun _ -> "foo bar\n ba$0z" |> assertNothingChanged + testCase "end" <| fun _ -> "foo bar\n baz$0" |> assertNothingChanged ] + + testList + "F# Code" + (let text = + """ $0module$0 Foo$0 $0let $0a = 42$0 @@ -1525,89 +1764,104 @@ let b = $0a $0+ 5$0 $0printfn "$0Result=%i" b$0 """ - text - |> Cursors.iter - |> List.mapi (fun i t -> - testCase $"Cursor {i}" <| fun _ -> - t |> assertNothingChanged - ) - ) - ] - - let assertRemoveRange range expected text = - text - |> Text.remove range - |> Expect.equal "incorrect string after removing" (Ok expected) - let assertAfterRemovingIs expected textWithRangeCursors = - let (range, text) = - textWithRangeCursors - |> Text.trimTripleQuotation - |> Cursor.tryExtractRange - |> Option.defaultWith (fun _ -> failtest "No cursors") - assertRemoveRange range expected text - testList "remove inside single line" [ - testList "single line string" [ - testCase "remove everything" <| fun _ -> - let text = "$0foo bar baz$0" - let expected = "" - text |> assertAfterRemovingIs expected - testCase "remove start to end of first word" <| fun _ -> - let text = "$0foo$0 bar baz" - let expected = " bar baz" - text |> assertAfterRemovingIs expected - testCase "remove last word to end of string" <| fun _ -> - let text = "foo bar $0baz$0" - let expected = "foo bar " - text |> assertAfterRemovingIs expected - testCase "remove word in middle" <| fun _ -> - let text = "foo $0bar$0 baz" - let expected = "foo baz" - text |> assertAfterRemovingIs expected - testCase "remove a lot in middle" <| fun _ -> - let text = "f$0oo bar ba$0z" - let expected = "fz" - text |> assertAfterRemovingIs expected - ] - testList "three line string" [ - testCase "remove everything" <| fun _ -> - let text = "$0foo bar\nbaz\nlorem ipsum$0" - let expected = "" - text |> assertAfterRemovingIs expected - testCase "remove first line without line break" <| fun _ -> - // let text = "$0foo bar$0\nbaz\nlorem ipsum" - let expected = "\nbaz\nlorem ipsum" - // text |> assertAfterRemovingIs expected - let text = "foo bar\nbaz\nlorem ipsum" - let range = range (pos 0 0) (pos 0 7) - text |> assertRemoveRange range expected - testCase "remove first line with line break" <| fun _ -> - // strictly speaking this removes over two lines... - // let text = "$0foo bar\n$0baz\nlorem ipsum" - let expected = "baz\nlorem ipsum" - // text |> assertAfterRemovingIs expected - let text = "foo bar\nbaz\nlorem ipsum" - let range = range (pos 0 0) (pos 1 0) - text |> assertRemoveRange range expected - testCase "remove 2nd line without line breaks" <| fun _ -> - let text = "foo bar\n$0baz$0\nlorem ipsum" - let expected = "foo bar\n\nlorem ipsum" - text |> assertAfterRemovingIs expected - testCase "remove 2nd line with line breaks" <| fun _ -> - let text = "foo bar$0\nbaz\n$0lorem ipsum" - let expected = "foo barlorem ipsum" - text |> assertAfterRemovingIs expected - testCase "remove 3rd line without line break" <| fun _ -> - let text = "foo bar\nbaz\n$0lorem ipsum$0" - let expected = "foo bar\nbaz\n" - text |> assertAfterRemovingIs expected - testCase "remove 3rd line with line break" <| fun _ -> - let text = "foo bar\nbaz$0\nlorem ipsum$0" - let expected = "foo bar\nbaz" - text |> assertAfterRemovingIs expected - ] - testList "F# Code" [ - testCase "Remove empty line" <| fun _ -> - let text = """ + + text + |> Cursors.iter + |> List.mapi (fun i t -> testCase $"Cursor {i}" <| fun _ -> t |> assertNothingChanged)) ] + + let assertRemoveRange range expected text = + text + |> Text.remove range + |> Expect.equal "incorrect string after removing" (Ok expected) + + let assertAfterRemovingIs expected textWithRangeCursors = + let (range, text) = + textWithRangeCursors + |> Text.trimTripleQuotation + |> Cursor.tryExtractRange + |> Option.defaultWith (fun _ -> failtest "No cursors") + + assertRemoveRange range expected text + + testList + "remove inside single line" + [ testList + "single line string" + [ testCase "remove everything" + <| fun _ -> + let text = "$0foo bar baz$0" + let expected = "" + text |> assertAfterRemovingIs expected + testCase "remove start to end of first word" + <| fun _ -> + let text = "$0foo$0 bar baz" + let expected = " bar baz" + text |> assertAfterRemovingIs expected + testCase "remove last word to end of string" + <| fun _ -> + let text = "foo bar $0baz$0" + let expected = "foo bar " + text |> assertAfterRemovingIs expected + testCase "remove word in middle" + <| fun _ -> + let text = "foo $0bar$0 baz" + let expected = "foo baz" + text |> assertAfterRemovingIs expected + testCase "remove a lot in middle" + <| fun _ -> + let text = "f$0oo bar ba$0z" + let expected = "fz" + text |> assertAfterRemovingIs expected ] + testList + "three line string" + [ testCase "remove everything" + <| fun _ -> + let text = "$0foo bar\nbaz\nlorem ipsum$0" + let expected = "" + text |> assertAfterRemovingIs expected + testCase "remove first line without line break" + <| fun _ -> + // let text = "$0foo bar$0\nbaz\nlorem ipsum" + let expected = "\nbaz\nlorem ipsum" + // text |> assertAfterRemovingIs expected + let text = "foo bar\nbaz\nlorem ipsum" + let range = range (pos 0 0) (pos 0 7) + text |> assertRemoveRange range expected + testCase "remove first line with line break" + <| fun _ -> + // strictly speaking this removes over two lines... + // let text = "$0foo bar\n$0baz\nlorem ipsum" + let expected = "baz\nlorem ipsum" + // text |> assertAfterRemovingIs expected + let text = "foo bar\nbaz\nlorem ipsum" + let range = range (pos 0 0) (pos 1 0) + text |> assertRemoveRange range expected + testCase "remove 2nd line without line breaks" + <| fun _ -> + let text = "foo bar\n$0baz$0\nlorem ipsum" + let expected = "foo bar\n\nlorem ipsum" + text |> assertAfterRemovingIs expected + testCase "remove 2nd line with line breaks" + <| fun _ -> + let text = "foo bar$0\nbaz\n$0lorem ipsum" + let expected = "foo barlorem ipsum" + text |> assertAfterRemovingIs expected + testCase "remove 3rd line without line break" + <| fun _ -> + let text = "foo bar\nbaz\n$0lorem ipsum$0" + let expected = "foo bar\nbaz\n" + text |> assertAfterRemovingIs expected + testCase "remove 3rd line with line break" + <| fun _ -> + let text = "foo bar\nbaz$0\nlorem ipsum$0" + let expected = "foo bar\nbaz" + text |> assertAfterRemovingIs expected ] + testList + "F# Code" + [ testCase "Remove empty line" + <| fun _ -> + let text = + """ module Foo $0 $0let a = 42 @@ -1615,16 +1869,21 @@ let b = a + 5 printfn "Result=%i" b """ - let expected = !- """ + + let expected = + !- """ module Foo let a = 42 let b = a + 5 printfn "Result=%i" b """ - text |> assertAfterRemovingIs expected - testCase "remove word" <| fun _ -> - let text = """ + + text |> assertAfterRemovingIs expected + testCase "remove word" + <| fun _ -> + let text = + """ module Foo let a = 42 @@ -1632,7 +1891,9 @@ let $0b$0 = a + 5 printfn "Result=%i" b """ - let expected = !- """ + + let expected = + !- """ module Foo let a = 42 @@ -1640,9 +1901,12 @@ let = a + 5 printfn "Result=%i" b """ - text |> assertAfterRemovingIs expected - testCase "remove end" <| fun _ -> - let text = """ + + text |> assertAfterRemovingIs expected + testCase "remove end" + <| fun _ -> + let text = + """ module Foo let a = 42 @@ -1650,7 +1914,9 @@ let b = a + 5 printfn "$0Result=%i" b$0 """ - let expected = !- """ + + let expected = + !- """ module Foo let a = 42 @@ -1658,9 +1924,12 @@ let b = a + 5 printfn " """ - text |> assertAfterRemovingIs expected - testCase "remove start" <| fun _ -> - let text = """ + + text |> assertAfterRemovingIs expected + testCase "remove start" + <| fun _ -> + let text = + """ $0module $0Foo let a = 42 @@ -1668,7 +1937,9 @@ let b = a + 5 printfn "Result=%i" b """ - let expected = !- """ + + let expected = + !- """ Foo let a = 42 @@ -1676,23 +1947,30 @@ let b = a + 5 printfn "Result=%i" b """ - text |> assertAfterRemovingIs expected - ] - ] - testList "remove over multiple lines" [ - testList "F# Code" [ - testCase "remove everything" <| fun _ -> - let text = """ + + text |> assertAfterRemovingIs expected ] ] + + testList + "remove over multiple lines" + [ testList + "F# Code" + [ testCase "remove everything" + <| fun _ -> + let text = + """ $0module Foo let a = 42 let b = a + 5 printfn "Result=%i" b$0""" - let expected = "" - text |> assertAfterRemovingIs expected - testCase "remove everything except last line break" <| fun _ -> - let text = """ + + let expected = "" + text |> assertAfterRemovingIs expected + testCase "remove everything except last line break" + <| fun _ -> + let text = + """ $0module Foo let a = 42 @@ -1700,10 +1978,13 @@ let b = a + 5 printfn "Result=%i" b$0 """ - let expected = "\n" - text |> assertAfterRemovingIs expected - testCase "remove lines 3-5" <| fun _ -> - let text = """ + + let expected = "\n" + text |> assertAfterRemovingIs expected + testCase "remove lines 3-5" + <| fun _ -> + let text = + """ module Foo $0let a = 42 @@ -1711,14 +1992,19 @@ let b = a + 5 $0printfn "Result=%i" b """ - let expected = !-""" + + let expected = + !- """ module Foo printfn "Result=%i" b """ - text |> assertAfterRemovingIs expected - testCase "remove lines from inside line 3 to inside line 5" <| fun _ -> - let text = """ + + text |> assertAfterRemovingIs expected + testCase "remove lines from inside line 3 to inside line 5" + <| fun _ -> + let text = + """ module Foo let a = $042 @@ -1726,15 +2012,20 @@ let b = a + $05 printfn "Result=%i" b """ - let expected = !-""" + + let expected = + !- """ module Foo let a = 5 printfn "Result=%i" b """ - text |> assertAfterRemovingIs expected - testCase "remove start to inside line 4" <| fun _ -> - let text = """ + + text |> assertAfterRemovingIs expected + testCase "remove start to inside line 4" + <| fun _ -> + let text = + """ $0module Foo let a = 42 @@ -1742,76 +2033,73 @@ let b $0= a + 5 printfn "Result=%i" b """ - let expected = !-""" + + let expected = + !- """ = a + 5 printfn "Result=%i" b """ - text |> assertAfterRemovingIs expected - testCase "remove inside line 4 to end" <| fun _ -> - let text = """ + + text |> assertAfterRemovingIs expected + testCase "remove inside line 4 to end" + <| fun _ -> + let text = + """ module Foo let a = 42 let b $0= a + 5 printfn "Result=%i" b$0""" - let expected = !-""" + + let expected = + !- """ module Foo let a = 42 let b """ - text |> assertAfterRemovingIs expected - ] - ] - ] - - let private insertTests = testList (nameof Text.insert) [ - testList "empty insert should insert nothing" [ - let assertNothingChanged textWithCursor = - let (pos, text) = - textWithCursor - |> Text.trimTripleQuotation - |> Cursor.tryExtractPosition - |> Option.defaultWith (fun _ -> failtest $"no cursor found") - let expected = Ok text - text - |> Text.insert pos "" - |> Expect.equal "shouldn't have changed input string" expected - - testCase "into empty string" <| fun _ -> - "$0" - |> assertNothingChanged - testList "into single line" [ - testCase "start" <| fun _ -> - "$0foo bar baz" - |> assertNothingChanged - testCase "middle" <| fun _ -> - "foo ba$0r baz" - |> assertNothingChanged - testCase "end" <| fun _ -> - "foo bar baz$0" - |> assertNothingChanged - ] - testList "into three lines" [ - testCase "start" <| fun _ -> - "$0foo\nbar\nbaz" - |> assertNothingChanged - testCase "start 2nd line" <| fun _ -> - "foo\n$0bar\nbaz" - |> assertNothingChanged - testCase "middle 2nd line" <| fun _ -> - "foo\nba$0r\nbaz" - |> assertNothingChanged - testCase "end 2nd line" <| fun _ -> - "foo\nbar$0\nbaz" - |> assertNothingChanged - testCase "end" <| fun _ -> - "foo\nbar\nbaz$0" - |> assertNothingChanged - ] - testList "into F# Code" ( - let text = """ + + text |> assertAfterRemovingIs expected ] ] ] + + let private insertTests = + testList + (nameof Text.insert) + [ testList + "empty insert should insert nothing" + [ let assertNothingChanged textWithCursor = + let (pos, text) = + textWithCursor + |> Text.trimTripleQuotation + |> Cursor.tryExtractPosition + |> Option.defaultWith (fun _ -> failtest $"no cursor found") + + let expected = Ok text + + text + |> Text.insert pos "" + |> Expect.equal "shouldn't have changed input string" expected + + testCase "into empty string" <| fun _ -> "$0" |> assertNothingChanged + + testList + "into single line" + [ testCase "start" <| fun _ -> "$0foo bar baz" |> assertNothingChanged + testCase "middle" <| fun _ -> "foo ba$0r baz" |> assertNothingChanged + testCase "end" <| fun _ -> "foo bar baz$0" |> assertNothingChanged ] + + testList + "into three lines" + [ testCase "start" <| fun _ -> "$0foo\nbar\nbaz" |> assertNothingChanged + testCase "start 2nd line" <| fun _ -> "foo\n$0bar\nbaz" |> assertNothingChanged + testCase "middle 2nd line" <| fun _ -> "foo\nba$0r\nbaz" |> assertNothingChanged + testCase "end 2nd line" <| fun _ -> "foo\nbar$0\nbaz" |> assertNothingChanged + testCase "end" <| fun _ -> "foo\nbar\nbaz$0" |> assertNothingChanged ] + + testList + "into F# Code" + (let text = + """ $0module$0 Foo$0 $0let $0a = 42$0 @@ -1819,54 +2107,56 @@ let b = $0a $0+ 5$0 $0printfn "$0Result=%i" b$0 """ - text - |> Cursors.iter - |> List.mapi (fun i t -> - testCase $"Cursor {i}" <| fun _ -> - t |> assertNothingChanged - ) - ) - ] - - let assertAfterInsertingIs expected (textWithCursor, insert) = - let (pos, text) = - textWithCursor - |> Text.trimTripleQuotation - |> Cursor.tryExtractPosition - |> Option.defaultWith (fun _ -> failtest "No cursor") - text - |> Text.insert pos insert - |> Expect.equal "incorrect string after inserting" (Ok expected) - testList "insert without linebreak" [ - testCase "into empty string" <| fun _ -> - let text = "$0" - let insert = "some text" - let expected = insert - (text,insert) - |> assertAfterInsertingIs expected - testList "into single line string" [ - testCase "start" <| fun _ -> - let text = "$0foo bar baz" - let insert = "some text" - let expected = $"{insert}foo bar baz" - (text,insert) - |> assertAfterInsertingIs expected - testCase "middle" <| fun _ -> - let text = "foo b$0ar baz" - let insert = "some text" - let expected = "foo bsome textar baz" - (text,insert) - |> assertAfterInsertingIs expected - testCase "end" <| fun _ -> - let text = "foo bar baz$0" - let insert = "some text" - let expected = $"foo bar baz{insert}" - (text,insert) - |> assertAfterInsertingIs expected - ] - testList "into F# Code" [ - testCase "start of 4th line" <| fun _ -> - let text = """ + + text + |> Cursors.iter + |> List.mapi (fun i t -> testCase $"Cursor {i}" <| fun _ -> t |> assertNothingChanged)) ] + + let assertAfterInsertingIs expected (textWithCursor, insert) = + let (pos, text) = + textWithCursor + |> Text.trimTripleQuotation + |> Cursor.tryExtractPosition + |> Option.defaultWith (fun _ -> failtest "No cursor") + + text + |> Text.insert pos insert + |> Expect.equal "incorrect string after inserting" (Ok expected) + + testList + "insert without linebreak" + [ testCase "into empty string" + <| fun _ -> + let text = "$0" + let insert = "some text" + let expected = insert + (text, insert) |> assertAfterInsertingIs expected + testList + "into single line string" + [ testCase "start" + <| fun _ -> + let text = "$0foo bar baz" + let insert = "some text" + let expected = $"{insert}foo bar baz" + (text, insert) |> assertAfterInsertingIs expected + testCase "middle" + <| fun _ -> + let text = "foo b$0ar baz" + let insert = "some text" + let expected = "foo bsome textar baz" + (text, insert) |> assertAfterInsertingIs expected + testCase "end" + <| fun _ -> + let text = "foo bar baz$0" + let insert = "some text" + let expected = $"foo bar baz{insert}" + (text, insert) |> assertAfterInsertingIs expected ] + testList + "into F# Code" + [ testCase "start of 4th line" + <| fun _ -> + let text = + """ module Foo let a = 42 @@ -1874,8 +2164,11 @@ $0let b = a + 5 printfn "Result=%i" b """ - let insert = "some text" - let expected = !- """ + + let insert = "some text" + + let expected = + !- """ module Foo let a = 42 @@ -1883,10 +2176,12 @@ some textlet b = a + 5 printfn "Result=%i" b """ - (text, insert) - |> assertAfterInsertingIs expected - testCase "middle of 4th line" <| fun _ -> - let text = """ + + (text, insert) |> assertAfterInsertingIs expected + testCase "middle of 4th line" + <| fun _ -> + let text = + """ module Foo let a = 42 @@ -1894,8 +2189,11 @@ let b$0 = a + 5 printfn "Result=%i" b """ - let insert = "some text" - let expected = !- """ + + let insert = "some text" + + let expected = + !- """ module Foo let a = 42 @@ -1903,10 +2201,12 @@ let bsome text = a + 5 printfn "Result=%i" b """ - (text, insert) - |> assertAfterInsertingIs expected - testCase "end of 4th line" <| fun _ -> - let text = """ + + (text, insert) |> assertAfterInsertingIs expected + testCase "end of 4th line" + <| fun _ -> + let text = + """ module Foo let a = 42 @@ -1914,8 +2214,11 @@ let b =$0 a + 5 printfn "Result=%i" b """ - let insert = "some text" - let expected = !- """ + + let insert = "some text" + + let expected = + !- """ module Foo let a = 42 @@ -1923,41 +2226,43 @@ let b =some text a + 5 printfn "Result=%i" b """ - (text, insert) - |> assertAfterInsertingIs expected - ] - ] - - testList "insert with line break" [ - testCase "into empty string" <| fun _ -> - let text = "$0" - let insert = "lorem\nipsum" - let expected = insert - (text, insert) - |> assertAfterInsertingIs expected - testList "into single line string" [ - testCase "start" <| fun _ -> - let text = "$0foo bar baz" - let insert = "lorem\nipsum" - let expected = "lorem\nipsumfoo bar baz" - (text, insert) - |> assertAfterInsertingIs expected - testCase "middle" <| fun _ -> - let text = "foo b$0ar baz" - let insert = "lorem\nipsum" - let expected = "foo blorem\nipsumar baz" - (text, insert) - |> assertAfterInsertingIs expected - testCase "end" <| fun _ -> - let text = "foo bar baz$0" - let insert = "lorem\nipsum" - let expected = "foo bar bazlorem\nipsum" - (text, insert) - |> assertAfterInsertingIs expected - ] - testList "into F# Code" [ - testCase "start" <| fun _ -> - let text = """ + + (text, insert) |> assertAfterInsertingIs expected ] ] + + testList + "insert with line break" + [ testCase "into empty string" + <| fun _ -> + let text = "$0" + let insert = "lorem\nipsum" + let expected = insert + (text, insert) |> assertAfterInsertingIs expected + testList + "into single line string" + [ testCase "start" + <| fun _ -> + let text = "$0foo bar baz" + let insert = "lorem\nipsum" + let expected = "lorem\nipsumfoo bar baz" + (text, insert) |> assertAfterInsertingIs expected + testCase "middle" + <| fun _ -> + let text = "foo b$0ar baz" + let insert = "lorem\nipsum" + let expected = "foo blorem\nipsumar baz" + (text, insert) |> assertAfterInsertingIs expected + testCase "end" + <| fun _ -> + let text = "foo bar baz$0" + let insert = "lorem\nipsum" + let expected = "foo bar bazlorem\nipsum" + (text, insert) |> assertAfterInsertingIs expected ] + testList + "into F# Code" + [ testCase "start" + <| fun _ -> + let text = + """ $0module Foo let a = 42 @@ -1965,8 +2270,11 @@ let b = a + 5 printfn "Result=%i" b """ - let insert = "lorem\nipsum" - let expected = !- """ + + let insert = "lorem\nipsum" + + let expected = + !- """ lorem ipsummodule Foo @@ -1975,18 +2283,23 @@ let b = a + 5 printfn "Result=%i" b """ - (text, insert) - |> assertAfterInsertingIs expected - testCase "end" <| fun _ -> - let text = """ + + (text, insert) |> assertAfterInsertingIs expected + testCase "end" + <| fun _ -> + let text = + """ module Foo let a = 42 let b = a + 5 printfn "Result=%i" b$0""" - let insert = "lorem\nipsum" - let expected = !- """ + + let insert = "lorem\nipsum" + + let expected = + !- """ module Foo let a = 42 @@ -1994,10 +2307,12 @@ let b = a + 5 printfn "Result=%i" blorem ipsum""" - (text, insert) - |> assertAfterInsertingIs expected - testCase "end before line break" <| fun _ -> - let text = """ + + (text, insert) |> assertAfterInsertingIs expected + testCase "end before line break" + <| fun _ -> + let text = + """ module Foo let a = 42 @@ -2005,8 +2320,11 @@ let b = a + 5 printfn "Result=%i" b$0 """ - let insert = "lorem\nipsum" - let expected = !- """ + + let insert = "lorem\nipsum" + + let expected = + !- """ module Foo let a = 42 @@ -2015,10 +2333,12 @@ let b = printfn "Result=%i" blorem ipsum """ - (text, insert) - |> assertAfterInsertingIs expected - testCase "start of 4th line" <| fun _ -> - let text = """ + + (text, insert) |> assertAfterInsertingIs expected + testCase "start of 4th line" + <| fun _ -> + let text = + """ module Foo let a = 42 @@ -2026,8 +2346,11 @@ $0let b = a + 5 printfn "Result=%i" b """ - let insert = "lorem\nipsum" - let expected = !- """ + + let insert = "lorem\nipsum" + + let expected = + !- """ module Foo let a = 42 @@ -2036,10 +2359,12 @@ ipsumlet b = a + 5 printfn "Result=%i" b """ - (text, insert) - |> assertAfterInsertingIs expected - testCase "middle of 4th line" <| fun _ -> - let text = """ + + (text, insert) |> assertAfterInsertingIs expected + testCase "middle of 4th line" + <| fun _ -> + let text = + """ module Foo let a = 42 @@ -2047,8 +2372,11 @@ let b$0 = a + 5 printfn "Result=%i" b """ - let insert = "lorem\nipsum" - let expected = !- """ + + let insert = "lorem\nipsum" + + let expected = + !- """ module Foo let a = 42 @@ -2057,10 +2385,12 @@ ipsum = a + 5 printfn "Result=%i" b """ - (text, insert) - |> assertAfterInsertingIs expected - testCase "end of 4th line" <| fun _ -> - let text = """ + + (text, insert) |> assertAfterInsertingIs expected + testCase "end of 4th line" + <| fun _ -> + let text = + """ module Foo let a = 42 @@ -2068,8 +2398,11 @@ let b =$0 a + 5 printfn "Result=%i" b """ - let insert = "lorem\nipsum" - let expected = !- """ + + let insert = "lorem\nipsum" + + let expected = + !- """ module Foo let a = 42 @@ -2078,47 +2411,57 @@ ipsum a + 5 printfn "Result=%i" b """ - (text, insert) - |> assertAfterInsertingIs expected - ] - ] - ] - let private replaceTests = testList (nameof Text.replace) [ - testList "neither change nor insert" [ - let assertNothingChanged (textWithCursors, replacement) = - let (range, text) = - textWithCursors - |> Text.trimTripleQuotation - |> Cursor.tryExtractRange - |> Option.defaultWith (fun _ -> failtest $"no cursor(s) found") - let expected = Ok text - text - |> Text.replace range replacement - |> Expect.equal "shouldn't have changed input string" expected - testCase "insert empty string into empty string" <| fun _ -> - let text = "$0" - let replacement = "" - (text, replacement) - |> assertNothingChanged - testCase "replace single line text with same text" <| fun _ -> - let text = "$0foo bar baz$0" - let replacement = "foo bar baz" - (text, replacement) - |> assertNothingChanged - testCase "replace inside single line text with same text" <| fun _ -> - let text = "foo$0 bar$0 baz" - let replacement = " bar" - (text, replacement) - |> assertNothingChanged - testCase "insert empty string into single line string" <| fun _ -> - let text = "foo $0bar baz" - let replacement = "" - (text, replacement) - |> assertNothingChanged - testList "F# Code" [ - testCase "insert empty string" <| fun _ -> - let text = """ + (text, insert) |> assertAfterInsertingIs expected ] ] ] + + let private replaceTests = + testList + (nameof Text.replace) + [ testList + "neither change nor insert" + [ let assertNothingChanged (textWithCursors, replacement) = + let (range, text) = + textWithCursors + |> Text.trimTripleQuotation + |> Cursor.tryExtractRange + |> Option.defaultWith (fun _ -> failtest $"no cursor(s) found") + + let expected = Ok text + + text + |> Text.replace range replacement + |> Expect.equal "shouldn't have changed input string" expected + + testCase "insert empty string into empty string" + <| fun _ -> + let text = "$0" + let replacement = "" + (text, replacement) |> assertNothingChanged + + testCase "replace single line text with same text" + <| fun _ -> + let text = "$0foo bar baz$0" + let replacement = "foo bar baz" + (text, replacement) |> assertNothingChanged + + testCase "replace inside single line text with same text" + <| fun _ -> + let text = "foo$0 bar$0 baz" + let replacement = " bar" + (text, replacement) |> assertNothingChanged + + testCase "insert empty string into single line string" + <| fun _ -> + let text = "foo $0bar baz" + let replacement = "" + (text, replacement) |> assertNothingChanged + + testList + "F# Code" + [ testCase "insert empty string" + <| fun _ -> + let text = + """ module Foo let a = 42 @@ -2126,28 +2469,34 @@ let b$0 = a + 5 printfn "Result=%i" b """ - let replacement = "" - (text, replacement) - |> assertNothingChanged - testCase "replace everything with itself" <| fun _ -> - let text = """ + + let replacement = "" + (text, replacement) |> assertNothingChanged + testCase "replace everything with itself" + <| fun _ -> + let text = + """ $0module Foo let a = 42 let b = a + 5 printfn "Result=%i" b$0""" - let replacement = !- """ + + let replacement = + !- """ module Foo let a = 42 let b = a + 5 printfn "Result=%i" b""" - (text, replacement) - |> assertNothingChanged - testCase "replace inside with same string" <| fun _ -> - let text = """ + + (text, replacement) |> assertNothingChanged + testCase "replace inside with same string" + <| fun _ -> + let text = + """ module Foo let a = 42 @@ -2155,56 +2504,65 @@ let $0b = a +$0 5 printfn "Result=%i" b """ - let replacement = "b =\n a +" - (text, replacement) - |> assertNothingChanged - ] - ] - let assertAfterChangingIs expected (textWithCursors, replacement) = - let (range, text) = - textWithCursors - |> Text.trimTripleQuotation - |> Cursor.tryExtractRange - |> Option.defaultWith (fun _ -> failtest $"no cursor(s) found") - let expected = Ok expected - text - |> Text.replace range replacement - |> Expect.equal "unexpected change" expected - - testList "replace in F# Code" [ - testCase "delete everything" <| fun _ -> - let text = """ + + let replacement = "b =\n a +" + (text, replacement) |> assertNothingChanged ] ] + let assertAfterChangingIs expected (textWithCursors, replacement) = + let (range, text) = + textWithCursors + |> Text.trimTripleQuotation + |> Cursor.tryExtractRange + |> Option.defaultWith (fun _ -> failtest $"no cursor(s) found") + + let expected = Ok expected + + text + |> Text.replace range replacement + |> Expect.equal "unexpected change" expected + + testList + "replace in F# Code" + [ testCase "delete everything" + <| fun _ -> + let text = + """ $0module Foo let a = 42 let b = a + 5 printfn "Result=%i" b$0""" - let replacement = "" - let expected = "" - (text, replacement) - |> assertAfterChangingIs expected - testCase "replace everything" <| fun _ -> - let text = """ + + let replacement = "" + let expected = "" + (text, replacement) |> assertAfterChangingIs expected + testCase "replace everything" + <| fun _ -> + let text = + """ $0module Foo let a = 42 let b = a + 5 printfn "Result=%i" b$0""" - let replacement = !- """ + + let replacement = + !- """ module Blub 42 |> (+) 5 |> printfn "Result = %i" """ - let expected = replacement - (text, replacement) - |> assertAfterChangingIs expected - testCase "insert some lines" <| fun _ -> - let text = """ + let expected = replacement + (text, replacement) |> assertAfterChangingIs expected + + testCase "insert some lines" + <| fun _ -> + let text = + """ module Foo $0let a = 42 @@ -2212,11 +2570,15 @@ let b = a + 5 printfn "Result=%i" b """ - let replacement = !- """ + + let replacement = + !- """ let pi = 3.14 let pi2 = pi * pi """ - let expected = !- """ + + let expected = + !- """ module Foo let pi = 3.14 @@ -2226,480 +2588,456 @@ let b = a + 5 printfn "Result=%i" b """ - (text, replacement) - |> assertAfterChangingIs expected - ] - ] - let tests = testList (nameof Text) [ - removeTests - insertTests - replaceTests - ] + (text, replacement) |> assertAfterChangingIs expected ] ] + + let tests = testList (nameof Text) [ removeTests; insertTests; replaceTests ] module private TextEdit = /// FSAC might return TextEdits with NewLine matching the OS /// but tests here only handle `\n` not `\r` /// -> test `TextEdit.apply` replaces/removes `\r` - let private eolTests = testList "EOL" [ - let testEOLs baseName textWithRange (newTextWithN: string) expected = - testList baseName [ - let expected = !- expected - for eol in ["\n"; "\r\n"; "\r"] do - let eolStr = System.Text.RegularExpressions.Regex.Escape eol - testCase $"with {eolStr}" <| fun _ -> - let (range, text) = Cursor.assertExtractRange <| !- textWithRange - let newText = newTextWithN.Replace("\n", eol) - let edit: TextEdit = { - NewText = newText - Range = range - } - - let actual = - text - |> TextEdit.apply edit - |> Flip.Expect.wantOk "Apply should not fail" - - Expect.equal actual expected "Apply should produce correct text" - Expect.isFalse (actual.Contains '\r') "Should not contain \\r" - ] - - testEOLs - "can apply insert edit" - """ + let private eolTests = + testList + "EOL" + [ let testEOLs baseName textWithRange (newTextWithN: string) expected = + testList + baseName + [ let expected = !-expected + + for eol in [ "\n"; "\r\n"; "\r" ] do + let eolStr = System.Text.RegularExpressions.Regex.Escape eol + + testCase $"with {eolStr}" + <| fun _ -> + let (range, text) = Cursor.assertExtractRange <| !-textWithRange + let newText = newTextWithN.Replace("\n", eol) + let edit: TextEdit = { NewText = newText; Range = range } + + let actual = + text |> TextEdit.apply edit |> Flip.Expect.wantOk "Apply should not fail" + + Expect.equal actual expected "Apply should produce correct text" + Expect.isFalse (actual.Contains '\r') "Should not contain \\r" ] + + testEOLs + "can apply insert edit" + """ let foo = 42$0 let bar = 2 """ - "\nlet baz = 4" - """ + "\nlet baz = 4" + """ let foo = 42 let baz = 4 let bar = 2 """ - testEOLs - "can apply delete edit" - """ + + testEOLs + "can apply delete edit" + """ let foo = $042 let bar = $02 """ - "" // kinda pointless: no new line in insert -> no new lines to replace... - """ + "" // kinda pointless: no new line in insert -> no new lines to replace... + """ let foo = 2 """ - testEOLs - "can apply replace edit" - """ + + testEOLs + "can apply replace edit" + """ let foo = $042 let bar =$0 2 """ - "3\nlet a = 1\nlet baz =" - """ + "3\nlet a = 1\nlet baz =" + """ let foo = 3 let a = 1 let baz = 2 - """ - ] - let private applyTests = testList (nameof TextEdit.apply) [ - eolTests - ] - - let private tryFindErrorTests = testList (nameof TextEdit.tryFindError) [ - testCase "valid delete edit should should be ok" <| fun _ -> - { - Range = { Start = pos 2 2; End = pos 3 3 } - NewText = "" - } - |> TextEdit.tryFindError - |> Flip.Expect.isNone "Valid delete should be ok" - testCase "valid insert edit should should be ok" <| fun _ -> - { - Range = { Start = pos 2 3; End = pos 2 3 } - NewText = "foo" - } - |> TextEdit.tryFindError - |> Flip.Expect.isNone "Valid delete should be ok" - testCase "valid replace edit should should be ok" <| fun _ -> - { - Range = { Start = pos 2 3; End = pos 4 9 } - NewText = "foo" - } - |> TextEdit.tryFindError - |> Flip.Expect.isNone "Valid delete should be ok" - testCase "empty edit should fail" <| fun _ -> - { - Range = { Start = pos 2 4; End = pos 2 4 } - NewText = "" - } - |> TextEdit.tryFindError - |> Flip.Expect.isSome "Empty edit should fail" - testCase "edit with End before Start should fail" <| fun _ -> - { - Range = { Start = pos 3 4; End = pos 2 2 } - NewText = "" - } - |> TextEdit.tryFindError - |> Flip.Expect.isSome "End before Start should fail" - ] - - let tests = testList (nameof TextEdit) [ - applyTests - tryFindErrorTests - ] + """ ] + + let private applyTests = testList (nameof TextEdit.apply) [ eolTests ] + + let private tryFindErrorTests = + testList + (nameof TextEdit.tryFindError) + [ testCase "valid delete edit should should be ok" + <| fun _ -> + { Range = { Start = pos 2 2; End = pos 3 3 } + NewText = "" } + |> TextEdit.tryFindError + |> Flip.Expect.isNone "Valid delete should be ok" + testCase "valid insert edit should should be ok" + <| fun _ -> + { Range = { Start = pos 2 3; End = pos 2 3 } + NewText = "foo" } + |> TextEdit.tryFindError + |> Flip.Expect.isNone "Valid delete should be ok" + testCase "valid replace edit should should be ok" + <| fun _ -> + { Range = { Start = pos 2 3; End = pos 4 9 } + NewText = "foo" } + |> TextEdit.tryFindError + |> Flip.Expect.isNone "Valid delete should be ok" + testCase "empty edit should fail" + <| fun _ -> + { Range = { Start = pos 2 4; End = pos 2 4 } + NewText = "" } + |> TextEdit.tryFindError + |> Flip.Expect.isSome "Empty edit should fail" + testCase "edit with End before Start should fail" + <| fun _ -> + { Range = { Start = pos 3 4; End = pos 2 2 } + NewText = "" } + |> TextEdit.tryFindError + |> Flip.Expect.isSome "End before Start should fail" ] + + let tests = testList (nameof TextEdit) [ applyTests; tryFindErrorTests ] module private TextEdits = - let sortByRangeTests = testList (nameof TextEdits.sortByRange) [ - let test (edits: TextEdit list) = - let sorted = edits |> TextEdits.sortByRange - Expect.equal (sorted.Length) (edits.Length) "Sorted edits should have same length as input edits" - - // must hold for all in sorted: - // * r <= succ(r) - // -> sorted - // * r = succ(r) -> Index(edits, r) < Index(edits, succ(r)) - // -> preserve order - let unsorted = - sorted - |> List.pairwise - |> List.filter (fun (r, succ) -> not <| Position.leq r.Range.Start succ.Range.Start) - // isEmpty doesn't print list when failure... - if not (unsorted |> List.isEmpty) then - logger.error ( - eventX "Unsorted: {list}" - >> setField "list" unsorted - ) - Expect.isEmpty unsorted "All edits should be sorted" - - // Note: for this to work edits must be different (-> different NewText) - let idxInEdits (edit: TextEdit) = - edits |> List.findIndex ((=) edit) - - let unordered = - sorted - |> List.indexed - |> List.pairwise - |> List.filter (fun ((_, r), (_, succ)) -> r.Range.Start = succ.Range.Start) - |> List.choose (fun ((i1, e1), (i2, e2)) -> - let iSrc1, iSrc2 = (idxInEdits e1, idxInEdits e2) - assert(iSrc1 <> iSrc2) - if iSrc1 < iSrc2 then - None - else - {| - Edits = (e1, e2) - SourceIndicies = (iSrc1, iSrc2) - SortedIndices = (i1, i2) - |} - |> Some - ) - // isEmpty doesn't print list when failure... - if not (unordered |> List.isEmpty) then - logger.error ( - eventX "Unordered: {list}" - >> setField "list" unordered - ) - Expect.isEmpty unordered "Edits with same start should keep order" - - testCase "can sort distinct ranges" <| fun _ -> - [ - (1,5) - (1,1) - (3, 2) - (8, 5) - (5, 4) - (5, 6) - (4, 11) - (1,7) - ] - |> List.mapi (fun i (l,c) -> - // end doesn't really matter (no overlap allowed) - let start = { Line = l; Character = c } - { - Range = { Start = start; End = start } - NewText = $"{i}=({l},{c})" - } - ) - |> test - - testCase "can sort all same position ranges" <| fun _ -> - List.replicate 10 (2,4) - |> List.mapi (fun i (l,c) -> - // end doesn't really matter (no overlap allowed) - let start = { Line = l; Character = c } - { - Range = { Start = start; End = start } - NewText = $"{i}=({l},{c})" - } - ) - |> test - - testCase "can sort mix of same and different positions" <| fun _ -> - [ - (1,5) - (1,1) - (3, 2) - (5, 4) - (1,5) - (8, 5) - (5, 4) - (5, 6) - (4, 11) - (4, 11) - (1,7) - ] - |> List.mapi (fun i (l,c) -> - // end doesn't really matter (no overlap allowed) - let start = { Line = l; Character = c } - { - Range = { Start = start; End = start } - NewText = $"{i}=({l},{c})" - } - ) - |> test - ] - - let tryFindErrorTests = testList (nameof TextEdits.tryFindError) [ - testCase "valid single edit should succeed" <| fun _ -> - [ - { NewText = "foo"; Range = { Start = pos 1 2; End = pos 1 5 } } - ] - |> TextEdits.tryFindError - |> Flip.Expect.isNone "valid single edit should succeed" - testCase "valid multiple edits should succeed" <| fun _ -> - [ - { NewText = "foo"; Range = { Start = pos 1 2; End = pos 1 5 } } - { NewText = "bar"; Range = { Start = pos 5 2; End = pos 5 2 } } - { NewText = "baz"; Range = { Start = pos 2 2; End = pos 3 3 } } - ] - |> TextEdits.tryFindError - |> Flip.Expect.isNone "valid multiple edits should succeed" - testCase "no edit should fail" <| fun _ -> - TextEdits.tryFindError [] - |> Flip.Expect.isSome "No edit should fail" - let replace (start, fin) text : TextEdit = { - NewText = text - Range = { Start = start; End = fin } - } - let delete (start, fin) = replace (start, fin) "" - let insert pos text = replace (pos, pos) text - let empty pos = insert pos "" - /// used to mark edits that aren't main parts of the test, but instead are just used as 'surrounding' - /// -> `filler` used for tagging - let inline filler v = v - testCase "single empty edit should fail" <| fun _ -> - TextEdits.tryFindError [empty (pos 2 3)] - |> Flip.Expect.isSome "Empty edit should fail" - testCase "multiple empty edits should fail" <| fun _ -> - TextEdits.tryFindError [empty (pos 2 3); empty (pos 3 5); empty (pos 1 1)] - |> Flip.Expect.isSome "Empty edit should fail" - testCase "empty edit in list with valid edits should fail" <| fun _ -> - [ - filler <| replace (pos 1 2, pos 1 5) "0" - filler <| replace (pos 5 2, pos 5 2) "1" - empty (pos 1 7) - filler <| replace (pos 2 2, pos 3 3) "1" - ] - |> TextEdits.tryFindError - |> Flip.Expect.isSome "Empty edit should fail" - testCase "two overlapping edits (Back/Front) on one line should fail" <| fun _ -> - [ - replace (pos 1 2, pos 1 5) "front overlap" - replace (pos 1 3, pos 1 7) "back overlap" - ] - |> TextEdits.tryFindError - |> Flip.Expect.isSome "Overlapping edits should fail" - testCase "two overlapping edits (Front/Back) on one line should fail" <| fun _ -> - [ - replace (pos 1 3, pos 1 7) "back overlap" - replace (pos 1 2, pos 1 5) "front overlap" - ] - |> TextEdits.tryFindError - |> Flip.Expect.isSome "Overlapping edits should fail" - testCase "two overlapping edits (Back/Front) over multiple lines should fail" <| fun _ -> - [ - replace (pos 1 2, pos 3 5) "front overlap" - replace (pos 2 3, pos 5 7) "back overlap" - ] - |> TextEdits.tryFindError - |> Flip.Expect.isSome "Overlapping edits should fail" - testCase "two touching edits should succeed" <| fun _ -> - // valid because: cursor is between characters - // -> replace prior to (3,5); replace after (3,5) - // -> do not interfere with each other - [ - replace (pos 1 2, pos 3 5) "front" - replace (pos 3 5, pos 5 7) "back" - ] - |> TextEdits.tryFindError - |> Flip.Expect.isNone "Touching edits should succeed" - testCase "two overlapping edits (Front/Back) over multiple lines should fail" <| fun _ -> - [ - replace (pos 2 3, pos 5 7) "back overlap" - replace (pos 1 2, pos 3 5) "front overlap" - ] - |> TextEdits.tryFindError - |> Flip.Expect.isSome "Overlapping edits should fail" - testCase "overlapping edits (Back/Front) in list with valid edits should fail" <| fun _ -> - [ - filler <| replace (pos 1 1, pos 1 1) "0" - filler <| replace (pos 17 8, pos 19 8) "1" - replace (pos 1 2, pos 3 5) "front overlap" - filler <| replace (pos 7 5, pos 8 9) "2" - replace (pos 2 3, pos 5 7) "back overlap" - filler <| replace (pos 11 1, pos 15 9) "3" - ] - |> TextEdits.tryFindError - |> Flip.Expect.isSome "Overlapping edits should fail" - testCase "replace inside another replace should fail" <| fun _ -> - [ - replace (pos 2 3, pos 4 1) "inside" - replace (pos 1 2, pos 5 7) "outside" - ] - |> TextEdits.tryFindError - |> Flip.Expect.isSome "Inside edits should fail" - testCase "replace with another replace inside should fail" <| fun _ -> - [ - replace (pos 1 2, pos 5 7) "outside" - replace (pos 2 3, pos 4 1) "inside" - ] - |> TextEdits.tryFindError - |> Flip.Expect.isSome "Inside edits should fail" - testCase "inserts with same position should succeed" <| fun _ -> - [ - insert (pos 2 4) "insert 1" - insert (pos 2 4) "insert 2" - ] - |> TextEdits.tryFindError - |> Flip.Expect.isNone "Same position inserts should succeed" - testCase "inserts with same position followed by replace starting at same position should succeed" <| fun _ -> - [ - insert (pos 2 4) "insert 1" - insert (pos 2 4) "insert 2" - replace (pos 2 4, pos 4 7) "replace" - ] - |> TextEdits.tryFindError - |> Flip.Expect.isNone "Same position inserts followed by replace should succeed" - testCase "replace before insert on same position should fail" <| fun _ -> - [ - replace (pos 2 4, pos 4 7) "replace" - insert (pos 2 4) "a" - ] - |> TextEdits.tryFindError - |> Flip.Expect.isSome "Replace before insert on same position should fail" - testCase "inserts with same position followed by replace at same position intermingled with other valid edits should succeed" <| fun _ -> - [ - filler <| replace (pos 6 7, pos 7 9) "0" - insert (pos 2 4) "insert 1" - filler <| replace (pos 1 4, pos 2 1) "1" - filler <| replace (pos 11 17, pos 18 19) "2" - insert (pos 2 4) "insert 2" - filler <| replace (pos 6 1, pos 6 2) "3" - replace (pos 2 4, pos 4 7) "replace" - filler <| replace (pos 9 2, pos 9 7) "4" - ] - |> TextEdits.tryFindError - |> Flip.Expect.isNone "Same position inserts followed by replace should succeed" - testCase "replace before insert on same position intermingled with other valid edits should fail" <| fun _ -> - [ - filler <| replace (pos 6 7, pos 7 9) "0" - insert (pos 2 4) "insert 1" - filler <| replace (pos 1 4, pos 2 1) "1" - filler <| replace (pos 11 17, pos 18 19) "2" - replace (pos 2 4, pos 4 7) "replace" - filler <| replace (pos 6 1, pos 6 2) "3" - insert (pos 2 4) "insert 2" - filler <| replace (pos 9 2, pos 9 7) "4" - ] - |> TextEdits.tryFindError - |> Flip.Expect.isSome "Replace before insert on same position should fail" - testCase "two replaces in same position should fail" <| fun _ -> - [ - replace (pos 2 4, pos 5 9) "replace 1" - replace (pos 2 4, pos 4 7) "replace 2" - ] - |> TextEdits.tryFindError - |> Flip.Expect.isSome "Two replaces in same position should fail" - testCase "two replaces in same position intermingled with other valid edits should fail should fail" <| fun _ -> - [ - filler <| replace (pos 6 7, pos 7 9) "0" - replace (pos 2 4, pos 5 9) "replace 1" - filler <| replace (pos 1 4, pos 2 1) "1" - replace (pos 2 4, pos 4 7) "replace 2" - filler <| replace (pos 6 1, pos 6 2) "2" - ] - |> TextEdits.tryFindError - |> Flip.Expect.isSome "Two replaces in same position should fail" - ] - - let applyTests = testList (nameof TextEdits.apply) [ - testList "single edit" [ - testCase "insert" <| fun _ -> - let (range, text) = Cursor.assertExtractRange !- """ + let sortByRangeTests = + testList + (nameof TextEdits.sortByRange) + [ let test (edits: TextEdit list) = + let sorted = edits |> TextEdits.sortByRange + Expect.equal (sorted.Length) (edits.Length) "Sorted edits should have same length as input edits" + + // must hold for all in sorted: + // * r <= succ(r) + // -> sorted + // * r = succ(r) -> Index(edits, r) < Index(edits, succ(r)) + // -> preserve order + let unsorted = + sorted + |> List.pairwise + |> List.filter (fun (r, succ) -> not <| Position.leq r.Range.Start succ.Range.Start) + // isEmpty doesn't print list when failure... + if not (unsorted |> List.isEmpty) then + logger.error (eventX "Unsorted: {list}" >> setField "list" unsorted) + + Expect.isEmpty unsorted "All edits should be sorted" + + // Note: for this to work edits must be different (-> different NewText) + let idxInEdits (edit: TextEdit) = edits |> List.findIndex ((=) edit) + + let unordered = + sorted + |> List.indexed + |> List.pairwise + |> List.filter (fun ((_, r), (_, succ)) -> r.Range.Start = succ.Range.Start) + |> List.choose (fun ((i1, e1), (i2, e2)) -> + let iSrc1, iSrc2 = (idxInEdits e1, idxInEdits e2) + assert (iSrc1 <> iSrc2) + + if iSrc1 < iSrc2 then + None + else + {| Edits = (e1, e2) + SourceIndicies = (iSrc1, iSrc2) + SortedIndices = (i1, i2) |} + |> Some) + // isEmpty doesn't print list when failure... + if not (unordered |> List.isEmpty) then + logger.error (eventX "Unordered: {list}" >> setField "list" unordered) + + Expect.isEmpty unordered "Edits with same start should keep order" + + testCase "can sort distinct ranges" + <| fun _ -> + [ (1, 5); (1, 1); (3, 2); (8, 5); (5, 4); (5, 6); (4, 11); (1, 7) ] + |> List.mapi (fun i (l, c) -> + // end doesn't really matter (no overlap allowed) + let start = { Line = l; Character = c } + + { Range = { Start = start; End = start } + NewText = $"{i}=({l},{c})" }) + |> test + + testCase "can sort all same position ranges" + <| fun _ -> + List.replicate 10 (2, 4) + |> List.mapi (fun i (l, c) -> + // end doesn't really matter (no overlap allowed) + let start = { Line = l; Character = c } + + { Range = { Start = start; End = start } + NewText = $"{i}=({l},{c})" }) + |> test + + testCase "can sort mix of same and different positions" + <| fun _ -> + [ (1, 5) + (1, 1) + (3, 2) + (5, 4) + (1, 5) + (8, 5) + (5, 4) + (5, 6) + (4, 11) + (4, 11) + (1, 7) ] + |> List.mapi (fun i (l, c) -> + // end doesn't really matter (no overlap allowed) + let start = { Line = l; Character = c } + + { Range = { Start = start; End = start } + NewText = $"{i}=({l},{c})" }) + |> test ] + + let tryFindErrorTests = + testList + (nameof TextEdits.tryFindError) + [ testCase "valid single edit should succeed" + <| fun _ -> + [ { NewText = "foo" + Range = { Start = pos 1 2; End = pos 1 5 } } ] + |> TextEdits.tryFindError + |> Flip.Expect.isNone "valid single edit should succeed" + testCase "valid multiple edits should succeed" + <| fun _ -> + [ { NewText = "foo" + Range = { Start = pos 1 2; End = pos 1 5 } } + { NewText = "bar" + Range = { Start = pos 5 2; End = pos 5 2 } } + { NewText = "baz" + Range = { Start = pos 2 2; End = pos 3 3 } } ] + |> TextEdits.tryFindError + |> Flip.Expect.isNone "valid multiple edits should succeed" + testCase "no edit should fail" + <| fun _ -> TextEdits.tryFindError [] |> Flip.Expect.isSome "No edit should fail" + let replace (start, fin) text : TextEdit = + { NewText = text + Range = { Start = start; End = fin } } + + let _delete (start, fin) = replace (start, fin) "" + let insert pos text = replace (pos, pos) text + let empty pos = insert pos "" + /// used to mark edits that aren't main parts of the test, but instead are just used as 'surrounding' + /// -> `filler` used for tagging + let inline filler v = v + + testCase "single empty edit should fail" + <| fun _ -> + TextEdits.tryFindError [ empty (pos 2 3) ] + |> Flip.Expect.isSome "Empty edit should fail" + + testCase "multiple empty edits should fail" + <| fun _ -> + TextEdits.tryFindError [ empty (pos 2 3); empty (pos 3 5); empty (pos 1 1) ] + |> Flip.Expect.isSome "Empty edit should fail" + + testCase "empty edit in list with valid edits should fail" + <| fun _ -> + [ filler <| replace (pos 1 2, pos 1 5) "0" + filler <| replace (pos 5 2, pos 5 2) "1" + empty (pos 1 7) + filler <| replace (pos 2 2, pos 3 3) "1" ] + |> TextEdits.tryFindError + |> Flip.Expect.isSome "Empty edit should fail" + + testCase "two overlapping edits (Back/Front) on one line should fail" + <| fun _ -> + [ replace (pos 1 2, pos 1 5) "front overlap" + replace (pos 1 3, pos 1 7) "back overlap" ] + |> TextEdits.tryFindError + |> Flip.Expect.isSome "Overlapping edits should fail" + + testCase "two overlapping edits (Front/Back) on one line should fail" + <| fun _ -> + [ replace (pos 1 3, pos 1 7) "back overlap" + replace (pos 1 2, pos 1 5) "front overlap" ] + |> TextEdits.tryFindError + |> Flip.Expect.isSome "Overlapping edits should fail" + + testCase "two overlapping edits (Back/Front) over multiple lines should fail" + <| fun _ -> + [ replace (pos 1 2, pos 3 5) "front overlap" + replace (pos 2 3, pos 5 7) "back overlap" ] + |> TextEdits.tryFindError + |> Flip.Expect.isSome "Overlapping edits should fail" + + testCase "two touching edits should succeed" + <| fun _ -> + // valid because: cursor is between characters + // -> replace prior to (3,5); replace after (3,5) + // -> do not interfere with each other + [ replace (pos 1 2, pos 3 5) "front"; replace (pos 3 5, pos 5 7) "back" ] + |> TextEdits.tryFindError + |> Flip.Expect.isNone "Touching edits should succeed" + + testCase "two overlapping edits (Front/Back) over multiple lines should fail" + <| fun _ -> + [ replace (pos 2 3, pos 5 7) "back overlap" + replace (pos 1 2, pos 3 5) "front overlap" ] + |> TextEdits.tryFindError + |> Flip.Expect.isSome "Overlapping edits should fail" + + testCase "overlapping edits (Back/Front) in list with valid edits should fail" + <| fun _ -> + [ filler <| replace (pos 1 1, pos 1 1) "0" + filler <| replace (pos 17 8, pos 19 8) "1" + replace (pos 1 2, pos 3 5) "front overlap" + filler <| replace (pos 7 5, pos 8 9) "2" + replace (pos 2 3, pos 5 7) "back overlap" + filler <| replace (pos 11 1, pos 15 9) "3" ] + |> TextEdits.tryFindError + |> Flip.Expect.isSome "Overlapping edits should fail" + + testCase "replace inside another replace should fail" + <| fun _ -> + [ replace (pos 2 3, pos 4 1) "inside"; replace (pos 1 2, pos 5 7) "outside" ] + |> TextEdits.tryFindError + |> Flip.Expect.isSome "Inside edits should fail" + + testCase "replace with another replace inside should fail" + <| fun _ -> + [ replace (pos 1 2, pos 5 7) "outside"; replace (pos 2 3, pos 4 1) "inside" ] + |> TextEdits.tryFindError + |> Flip.Expect.isSome "Inside edits should fail" + + testCase "inserts with same position should succeed" + <| fun _ -> + [ insert (pos 2 4) "insert 1"; insert (pos 2 4) "insert 2" ] + |> TextEdits.tryFindError + |> Flip.Expect.isNone "Same position inserts should succeed" + + testCase "inserts with same position followed by replace starting at same position should succeed" + <| fun _ -> + [ insert (pos 2 4) "insert 1" + insert (pos 2 4) "insert 2" + replace (pos 2 4, pos 4 7) "replace" ] + |> TextEdits.tryFindError + |> Flip.Expect.isNone "Same position inserts followed by replace should succeed" + + testCase "replace before insert on same position should fail" + <| fun _ -> + [ replace (pos 2 4, pos 4 7) "replace"; insert (pos 2 4) "a" ] + |> TextEdits.tryFindError + |> Flip.Expect.isSome "Replace before insert on same position should fail" + + testCase + "inserts with same position followed by replace at same position intermingled with other valid edits should succeed" + <| fun _ -> + [ filler <| replace (pos 6 7, pos 7 9) "0" + insert (pos 2 4) "insert 1" + filler <| replace (pos 1 4, pos 2 1) "1" + filler <| replace (pos 11 17, pos 18 19) "2" + insert (pos 2 4) "insert 2" + filler <| replace (pos 6 1, pos 6 2) "3" + replace (pos 2 4, pos 4 7) "replace" + filler <| replace (pos 9 2, pos 9 7) "4" ] + |> TextEdits.tryFindError + |> Flip.Expect.isNone "Same position inserts followed by replace should succeed" + + testCase "replace before insert on same position intermingled with other valid edits should fail" + <| fun _ -> + [ filler <| replace (pos 6 7, pos 7 9) "0" + insert (pos 2 4) "insert 1" + filler <| replace (pos 1 4, pos 2 1) "1" + filler <| replace (pos 11 17, pos 18 19) "2" + replace (pos 2 4, pos 4 7) "replace" + filler <| replace (pos 6 1, pos 6 2) "3" + insert (pos 2 4) "insert 2" + filler <| replace (pos 9 2, pos 9 7) "4" ] + |> TextEdits.tryFindError + |> Flip.Expect.isSome "Replace before insert on same position should fail" + + testCase "two replaces in same position should fail" + <| fun _ -> + [ replace (pos 2 4, pos 5 9) "replace 1" + replace (pos 2 4, pos 4 7) "replace 2" ] + |> TextEdits.tryFindError + |> Flip.Expect.isSome "Two replaces in same position should fail" + + testCase "two replaces in same position intermingled with other valid edits should fail should fail" + <| fun _ -> + [ filler <| replace (pos 6 7, pos 7 9) "0" + replace (pos 2 4, pos 5 9) "replace 1" + filler <| replace (pos 1 4, pos 2 1) "1" + replace (pos 2 4, pos 4 7) "replace 2" + filler <| replace (pos 6 1, pos 6 2) "2" ] + |> TextEdits.tryFindError + |> Flip.Expect.isSome "Two replaces in same position should fail" ] + + let applyTests = + testList + (nameof TextEdits.apply) + [ testList + "single edit" + [ testCase "insert" + <| fun _ -> + let (range, text) = + Cursor.assertExtractRange + !- """ let foo = 42$0 let bar = 2 """ - let edit: TextEdit = { - NewText = "\nlet baz = 4" - Range = range - } - let expected = !- """ + + let edit: TextEdit = + { NewText = "\nlet baz = 4" + Range = range } + + let expected = + !- """ let foo = 42 let baz = 4 let bar = 2 """ - let actual = - text - |> TextEdit.apply edit - |> Flip.Expect.wantOk "Apply should not fail" - Expect.equal actual expected "Apply should produce correct text" - testCase "remove" <| fun _ -> - let (range, text) = Cursor.assertExtractRange !- """ + + let actual = + text |> TextEdit.apply edit |> Flip.Expect.wantOk "Apply should not fail" + + Expect.equal actual expected "Apply should produce correct text" + testCase "remove" + <| fun _ -> + let (range, text) = + Cursor.assertExtractRange + !- """ let foo = $042 let bar = $02 """ - let edit: TextEdit = { - NewText = "" - Range = range - } - let expected = !- """ + + let edit: TextEdit = { NewText = ""; Range = range } + + let expected = + !- """ let foo = 2 """ - let actual = - text - |> TextEdit.apply edit - |> Flip.Expect.wantOk "Apply should not fail" - Expect.equal actual expected "Apply should produce correct text" - testCase "replace" <| fun _ -> - let (range, text) = Cursor.assertExtractRange !- """ + + let actual = + text |> TextEdit.apply edit |> Flip.Expect.wantOk "Apply should not fail" + + Expect.equal actual expected "Apply should produce correct text" + testCase "replace" + <| fun _ -> + let (range, text) = + Cursor.assertExtractRange + !- """ let foo = $042 let bar$0 = 2 """ - let edit: TextEdit = { - NewText = "1\nlet baz" - Range = range - } - let expected = !- """ + + let edit: TextEdit = + { NewText = "1\nlet baz" + Range = range } + + let expected = + !- """ let foo = 1 let baz = 2 """ - let actual = - text - |> TextEdit.apply edit - |> Flip.Expect.wantOk "Apply should not fail" - Expect.equal actual expected "Apply should produce correct text" - ] - ] - let tests = testList (nameof TextEdits) [ - sortByRangeTests - tryFindErrorTests - applyTests - ] - -let tests = testList (nameof TextEdit) [ - Cursor.tests - Cursors.tests - Text.tests - TextEdit.tests - TextEdits.tests -] + + let actual = + text |> TextEdit.apply edit |> Flip.Expect.wantOk "Apply should not fail" + + Expect.equal actual expected "Apply should produce correct text" ] ] + + let tests = + testList (nameof TextEdits) [ sortByRangeTests; tryFindErrorTests; applyTests ] + +let tests = + testList (nameof TextEdit) [ Cursor.tests; Cursors.tests; Text.tests; TextEdit.tests; TextEdits.tests ] diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/TextEdit.fs b/test/FsAutoComplete.Tests.Lsp/Utils/TextEdit.fs index 62372067b..fb06abb78 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/TextEdit.fs +++ b/test/FsAutoComplete.Tests.Lsp/Utils/TextEdit.fs @@ -1,4 +1,6 @@ module Utils.TextEdit + +open System open Ionide.LanguageServerProtocol.Types open Utils.Utils open Expecto @@ -49,7 +51,7 @@ module Cursor = /// Note: Cursor Position is BEFORE index. /// Note: Index might be `text.Length` (-> Cursor AFTER last char in text) let tryExtractIndex (text: string) = - match text.IndexOf Marker with + match text.IndexOf(Marker, StringComparison.Ordinal) with | -1 -> None | i -> (i, text.Remove(i, Marker.Length)) @@ -65,7 +67,7 @@ module Cursor = let markersInLine = markers |> Array.choose (fun marker -> - match line.IndexOf marker with + match line.IndexOf(marker, StringComparison.Ordinal) with | -1 -> None | column -> Some (marker, column) ) @@ -252,7 +254,7 @@ module Cursors = /// Note: doesn't trim input! let iter (textWithCursors: string) = let rec collect (textsWithSingleCursor) (textWithCursors: string) = - match textWithCursors.IndexOf Cursor.Marker with + match textWithCursors.IndexOf(Cursor.Marker, StringComparison.Ordinal) with | -1 -> textsWithSingleCursor |> List.rev | i -> let textWithSingleCursor = diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/Utils.fs b/test/FsAutoComplete.Tests.Lsp/Utils/Utils.fs index e039936d1..cb75e3b36 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/Utils.fs +++ b/test/FsAutoComplete.Tests.Lsp/Utils/Utils.fs @@ -4,53 +4,58 @@ module Expect = open FsAutoComplete.Utils open Expecto - let failureMatching (m: AssertException -> bool) (f: Async<_>) = async { - let failed = async { - try - do! f |> Async.map ignore - return false - with - | :? AssertException as ex when m ex -> return true - // keep other exceptions + let failureMatching (m: AssertException -> bool) (f: Async<_>) = + async { + let failed = + async { + try + do! f |> Async.map ignore + return false + with :? AssertException as ex when m ex -> + return true + // keep other exceptions + } + + let! failed = failed + + if not failed then + failtestf "Expected AssertException, but was no exception" } - let! failed = failed - if not failed then - failtestf "Expected AssertException, but was no exception" - } /// passed Async `f` is expected to throw `Expecto.AssertException` /// -> Expecto Test in `f` is expected to fail - /// + /// /// ~ Basically fancy `Async` wrapper for `Expect.throwsT` - /// + /// /// Note: `failwith` doesn't trigger success (throws `System.Exception`). Use `failtest` instead let failure f = failureMatching (fun _ -> true) f module private Seq = let tryMin source = source - |> Seq.fold (fun m e -> - match m with - | None -> Some e - | Some m -> Some (min m e) - ) None + |> Seq.fold + (fun m e -> + match m with + | None -> Some e + | Some m -> Some(min m e)) + None module Position = open Ionide.LanguageServerProtocol.Types let inline assertPositive (pos: Position) = - assert(pos.Line >= 0) - assert(pos.Character >= 0) + assert (pos.Line >= 0) + assert (pos.Character >= 0) - let inline eq p1 p2 = + let inline eq p1 p2 = // p1.Line = p2.Line && p1.Character = p2.Character p1 = p2 - let inline gt p1 p2 = - p1.Line > p2.Line || (p1.Line = p2.Line && p1.Character > p2.Character) + + let inline gt p1 p2 = p1.Line > p2.Line || (p1.Line = p2.Line && p1.Character > p2.Character) let inline geq p1 p2 = eq p1 p2 || gt p1 p2 let inline lt p1 p2 = gt p2 p1 let inline leq p1 p2 = geq p2 p1 - + /// Note: Always assumes correct order inside Range: `Start <= End` module Range = open Ionide.LanguageServerProtocol.Types @@ -60,11 +65,10 @@ module Range = /// Range represents a single position (`Start = End`) - let inline isPosition (range: Range) = - range.Start = range.End + let inline isPosition (range: Range) = range.Start = range.End /// Strict: `pos` on `Start` or `End` of `range` counts as containing - /// + /// /// ```text /// -----------------------------------> /// ^^^^^^^^^^^^^^^^^ range @@ -79,7 +83,7 @@ module Range = Position.leq range.Start pos && Position.leq pos range.End /// Loose: `pos` on `Start` or `End` of `range` doesn't count as containing - /// + /// /// ```text /// -----------------------------------> /// ^^^^^^^^^^^^^^^^^ range @@ -102,8 +106,7 @@ module Range = /// | ┕ true /// ┕ false /// ``` - let inline onBorder (pos: Position) (range: Range) = - pos = range.Start || pos = range.End + let inline onBorder (pos: Position) (range: Range) = pos = range.Start || pos = range.End /// Share a Start/End or End/Start, but nothing else. /// @@ -113,13 +116,12 @@ module Range = /// | | ^^^^^^^^ false /// | ^^^^^^^^ true /// ^^^^^^^^ false - /// ^^^^^^ false + /// ^^^^^^ false /// ^^^ false /// ^ true /// ^^^ true /// ``` - let inline touches (range1: Range) (range2: Range) = - range1.Start = range2.End || range1.End = range2.Start + let inline touches (range1: Range) (range2: Range) = range1.Start = range2.End || range1.End = range2.Start /// Strict: Just sharing a Start/End (touching) counts as overlap too /// @@ -129,27 +131,24 @@ module Range = /// | | ^^^^^^^^ false /// | ^^^^^^^^ true /// ^^^^^^^^ true - /// ^^^^^^^ true + /// ^^^^^^^ true /// ``` let overlapsStrictly (range1: Range) (range2: Range) = range1 |> containsStrictly range2.Start - || - range1 |> containsStrictly range2.End - || - range2 |> containsStrictly range1.Start - || - range2 |> containsStrictly range1.End - + || range1 |> containsStrictly range2.End + || range2 |> containsStrictly range1.Start + || range2 |> containsStrictly range1.End + /// Loose: Touching doesn't count as overlapping. /// Neither does both just position and same position - /// + /// /// ```text /// --------------------------> /// ^^^^^^^ /// | | | ^^^^^^^^ false /// | | ^^^^^^^^ false /// | ^^^^^^^^ true - /// ^^^^^^^ true + /// ^^^^^^^ true /// ``` /// ```text /// --------------------------> @@ -160,12 +159,10 @@ module Range = /// ^^^^^^ true /// ``` let overlapsLoosely (range1: Range) (range2: Range) = - (range1 |> overlapsStrictly range2) - && - not (range1 |> touches range2) + (range1 |> overlapsStrictly range2) && not (range1 |> touches range2) /// Strict: Touching is not disjoint - /// + /// /// ```text /// --------------------------> /// ^^^^^^^ @@ -174,10 +171,9 @@ module Range = /// | ^ false /// ^^^^^^^^ false /// ``` - let isDisjointStrictly (range1: Range) (range2: Range) = - not <| overlapsStrictly range1 range2 + let isDisjointStrictly (range1: Range) (range2: Range) = not <| overlapsStrictly range1 range2 /// Loose: Touching is disjoint - /// + /// /// ```text /// --------------------------> /// ^^^^^^^ @@ -186,8 +182,7 @@ module Range = /// | ^ true /// ^^^^^^^^ false /// ``` - let isDisjointLoosely (range1: Range) (range2: Range) = - not <| overlapsLoosely range1 range2 + let isDisjointLoosely (range1: Range) (range2: Range) = not <| overlapsLoosely range1 range2 module Text = @@ -196,45 +191,33 @@ module Text = let inline assertNoCarriageReturn (text: string) = if text.Contains '\r' then Expecto.Tests.failtest "Text contains `\\r` (either alone or as `\\r\\n`). But only `\\n` is supported" - let removeCarriageReturn (text: string) = - text.Replace("\r\n", "\n").Replace("\r", "\n") + + let removeCarriageReturn (text: string) = text.Replace("\r\n", "\n").Replace("\r", "\n") /// Note: only works with `\n`, but fails with `\r`! - let lines (text: string) = + let lines (text: string) = assertNoCarriageReturn text text.Split '\n' - /// remove leading `\n` from triple quoted string with text starting in next line - let private trimLeadingNewLine (text: string) = - if text.StartsWith '\n' then - text.Substring 1 - else - text - /// remove trailing whitespace from last line, if last line is otherwise empty. - /// Note: keeps the `\n`! + /// remove leading `\n` from triple quoted string with text starting in next line + let private trimLeadingNewLine (text: string) = if text.StartsWith '\n' then text.Substring 1 else text + + /// remove trailing whitespace from last line, if last line is otherwise empty. + /// Note: keeps the `\n`! /// Note: doesn't trim a single line with just whitespace -> requires at least one `\n` let private trimLastWhitespacesLine (text: string) = match text.LastIndexOf '\n' with | -1 -> text | i -> - let tail = text.AsSpan(i+1) - if not tail.IsEmpty && tail.IsWhiteSpace() then - text.Substring(0, i+1) - else - text - /// remove trailing last line, if last line is empty. - /// Unlike `trimLastWhitespacesLine` this removes the trailing `\n` too - /// Note: doesn't trim a single line with just whitespace -> requires at least one `\n` - let private trimTrailingEmptyLine (text: string) = - match text.LastIndexOf '\n' with - | -1 -> + let tail = text.AsSpan(i + 1) + + if not tail.IsEmpty && tail.IsWhiteSpace() then + text.Substring(0, i + 1) + else text - | i when text.AsSpan().Slice(i).IsWhiteSpace() -> // `\n` is whitespace - text.Substring(0, i) - | _ -> text - let getIndentation (line: string) = - line.Length - line.AsSpan().TrimStart().Length + let getIndentation (line: string) = line.Length - line.AsSpan().TrimStart().Length + let private detectIndentation (text: string) = text |> lines @@ -247,29 +230,25 @@ module Text = match text |> detectIndentation with | 0 -> text | ind -> - text - |> lines - |> Seq.map (fun line -> - if line.Length <= ind then - assert(line |> String.IsNullOrWhiteSpace) - "" - else - line.Substring ind - ) - |> String.concat "\n" - + text + |> lines + |> Seq.map (fun line -> + if line.Length <= ind then + assert (line |> String.IsNullOrWhiteSpace) + "" + else + line.Substring ind) + |> String.concat "\n" + /// Trim: /// * Leading `\n` from triple quotes string with text starting in next line /// * indentation (measured for non-empty lines) - /// * Trailing whitespace in otherwise empty last line + /// * Trailing whitespace in otherwise empty last line /// Note: `\n` isn't removed - /// + /// /// Note: Asserts the passed text contains no `\r` (neither `\r` nor `\r\n`). /// It doesn't replace `\r` with `\n` but instead fails! let trimTripleQuotation (text: string) = assertNoCarriageReturn text - text - |> trimLeadingNewLine - |> trimIndentation - |> trimLastWhitespacesLine + text |> trimLeadingNewLine |> trimIndentation |> trimLastWhitespacesLine diff --git a/test/FsAutoComplete.Tests.Lsp/paket.references b/test/FsAutoComplete.Tests.Lsp/paket.references index 0696c0d64..dd52fd1a2 100644 --- a/test/FsAutoComplete.Tests.Lsp/paket.references +++ b/test/FsAutoComplete.Tests.Lsp/paket.references @@ -2,7 +2,6 @@ FSharp.Core content: once FSharp.Compiler.Service FSharp.Control.Reactive FSharpx.Async -Expecto Expecto.Diff Microsoft.NET.Test.Sdk YoloDev.Expecto.TestSdk @@ -10,9 +9,8 @@ AltCover GitHubActionsTestLogger CliWrap FSharp.Data.Adaptive - -Microsoft.Build copy_local:false -Microsoft.Build.Framework copy_local:false -Microsoft.Build.Utilities.Core copy_local:false +Microsoft.Build copy_local: false +Microsoft.Build.Framework copy_local: false +Microsoft.Build.Utilities.Core copy_local: false Microsoft.Build.Tasks.Core copy_local: false -NuGet.Frameworks copy_local: false +NuGet.Frameworks copy_local: false \ No newline at end of file diff --git a/test/OptionAnalyzer/Analyzer.fs b/test/OptionAnalyzer/Analyzer.fs index 6517d243a..faca8bdb9 100644 --- a/test/OptionAnalyzer/Analyzer.fs +++ b/test/OptionAnalyzer/Analyzer.fs @@ -6,146 +6,147 @@ open FSharp.Compiler.Symbols open FSharp.Compiler.Text open FsAutoComplete.Logging -let rec visitExpr memberCallHandler (e:FSharpExpr) = - match e with - | FSharpExprPatterns.AddressOf(lvalueExpr) -> - visitExpr memberCallHandler lvalueExpr - | FSharpExprPatterns.AddressSet(lvalueExpr, rvalueExpr) -> - visitExpr memberCallHandler lvalueExpr; visitExpr memberCallHandler rvalueExpr - | FSharpExprPatterns.Application(funcExpr, typeArgs, argExprs) -> - visitExpr memberCallHandler funcExpr; visitExprs memberCallHandler argExprs - | FSharpExprPatterns.Call(objExprOpt, memberOrFunc, typeArgs1, typeArgs2, argExprs) -> - memberCallHandler e.Range memberOrFunc - visitObjArg memberCallHandler objExprOpt; visitExprs memberCallHandler argExprs - | FSharpExprPatterns.Coerce(targetType, inpExpr) -> - visitExpr memberCallHandler inpExpr - | FSharpExprPatterns.FastIntegerForLoop(startExpr, limitExpr, consumeExpr, isUp, _, _) -> - visitExpr memberCallHandler startExpr; visitExpr memberCallHandler limitExpr; visitExpr memberCallHandler consumeExpr - | FSharpExprPatterns.ILAsm(asmCode, typeArgs, argExprs) -> - visitExprs memberCallHandler argExprs - | FSharpExprPatterns.ILFieldGet (objExprOpt, fieldType, fieldName) -> - visitObjArg memberCallHandler objExprOpt - | FSharpExprPatterns.ILFieldSet (objExprOpt, fieldType, fieldName, valueExpr) -> - visitObjArg memberCallHandler objExprOpt - | FSharpExprPatterns.IfThenElse (guardExpr, thenExpr, elseExpr) -> - visitExpr memberCallHandler guardExpr; visitExpr memberCallHandler thenExpr; visitExpr memberCallHandler elseExpr - | FSharpExprPatterns.Lambda(lambdaVar, bodyExpr) -> - visitExpr memberCallHandler bodyExpr - | FSharpExprPatterns.Let((bindingVar, bindingExpr, _), bodyExpr) -> - visitExpr memberCallHandler bindingExpr; visitExpr memberCallHandler bodyExpr - | FSharpExprPatterns.LetRec(recursiveBindings, bodyExpr) -> - List.iter ((fun (_, x, _) -> x) >> visitExpr memberCallHandler) recursiveBindings; visitExpr memberCallHandler bodyExpr - | FSharpExprPatterns.NewArray(arrayType, argExprs) -> - visitExprs memberCallHandler argExprs - | FSharpExprPatterns.NewDelegate(delegateType, delegateBodyExpr) -> - visitExpr memberCallHandler delegateBodyExpr - | FSharpExprPatterns.NewObject(objType, typeArgs, argExprs) -> - visitExprs memberCallHandler argExprs - | FSharpExprPatterns.NewRecord(recordType, argExprs) -> - visitExprs memberCallHandler argExprs - | FSharpExprPatterns.NewTuple(tupleType, argExprs) -> - visitExprs memberCallHandler argExprs - | FSharpExprPatterns.NewUnionCase(unionType, unionCase, argExprs) -> - visitExprs memberCallHandler argExprs - | FSharpExprPatterns.Quote(quotedExpr) -> - visitExpr memberCallHandler quotedExpr - | FSharpExprPatterns.FSharpFieldGet(objExprOpt, recordOrClassType, fieldInfo) -> - visitObjArg memberCallHandler objExprOpt - | FSharpExprPatterns.FSharpFieldSet(objExprOpt, recordOrClassType, fieldInfo, argExpr) -> - visitObjArg memberCallHandler objExprOpt; visitExpr memberCallHandler argExpr - | FSharpExprPatterns.Sequential(firstExpr, secondExpr) -> - visitExpr memberCallHandler firstExpr; visitExpr memberCallHandler secondExpr - | FSharpExprPatterns.TryFinally(bodyExpr, finalizeExpr, _, _) -> - visitExpr memberCallHandler bodyExpr; visitExpr memberCallHandler finalizeExpr - | FSharpExprPatterns.TryWith(bodyExpr, _, _, catchVar, catchExpr, _, _) -> - visitExpr memberCallHandler bodyExpr; visitExpr memberCallHandler catchExpr - | FSharpExprPatterns.TupleGet(tupleType, tupleElemIndex, tupleExpr) -> - visitExpr memberCallHandler tupleExpr - | FSharpExprPatterns.DecisionTree(decisionExpr, decisionTargets) -> - visitExpr memberCallHandler decisionExpr; List.iter (snd >> visitExpr memberCallHandler) decisionTargets - | FSharpExprPatterns.DecisionTreeSuccess (decisionTargetIdx, decisionTargetExprs) -> - visitExprs memberCallHandler decisionTargetExprs - | FSharpExprPatterns.TypeLambda(genericParam, bodyExpr) -> - visitExpr memberCallHandler bodyExpr - | FSharpExprPatterns.TypeTest(ty, inpExpr) -> - visitExpr memberCallHandler inpExpr - | FSharpExprPatterns.UnionCaseSet(unionExpr, unionType, unionCase, unionCaseField, valueExpr) -> - visitExpr memberCallHandler unionExpr; visitExpr memberCallHandler valueExpr - | FSharpExprPatterns.UnionCaseGet(unionExpr, unionType, unionCase, unionCaseField) -> - visitExpr memberCallHandler unionExpr - | FSharpExprPatterns.UnionCaseTest(unionExpr, unionType, unionCase) -> - visitExpr memberCallHandler unionExpr - | FSharpExprPatterns.UnionCaseTag(unionExpr, unionType) -> - visitExpr memberCallHandler unionExpr - | FSharpExprPatterns.ObjectExpr(objType, baseCallExpr, overrides, interfaceImplementations) -> - visitExpr memberCallHandler baseCallExpr - List.iter (visitObjMember memberCallHandler) overrides - List.iter (snd >> List.iter (visitObjMember memberCallHandler)) interfaceImplementations - | FSharpExprPatterns.TraitCall(sourceTypes, traitName, typeArgs, typeInstantiation, argTypes, argExprs) -> - visitExprs memberCallHandler argExprs - | FSharpExprPatterns.ValueSet(valToSet, valueExpr) -> - visitExpr memberCallHandler valueExpr - | FSharpExprPatterns.WhileLoop(guardExpr, bodyExpr, _) -> - visitExpr memberCallHandler guardExpr; visitExpr memberCallHandler bodyExpr - | FSharpExprPatterns.BaseValue baseType -> () - | FSharpExprPatterns.DefaultValue defaultType -> () - | FSharpExprPatterns.ThisValue thisType -> () - | FSharpExprPatterns.Const(constValueObj, constType) -> () - | FSharpExprPatterns.Value(valueToGet) -> () - | _ -> () - -and visitExprs f exprs = - List.iter (visitExpr f) exprs - -and visitObjArg f objOpt = - Option.iter (visitExpr f) objOpt - -and visitObjMember f memb = - visitExpr f memb.Body +let rec visitExpr memberCallHandler (e: FSharpExpr) = + match e with + | FSharpExprPatterns.AddressOf(lvalueExpr) -> visitExpr memberCallHandler lvalueExpr + | FSharpExprPatterns.AddressSet(lvalueExpr, rvalueExpr) -> + visitExpr memberCallHandler lvalueExpr + visitExpr memberCallHandler rvalueExpr + | FSharpExprPatterns.Application(funcExpr, _, argExprs) -> + visitExpr memberCallHandler funcExpr + visitExprs memberCallHandler argExprs + | FSharpExprPatterns.Call(objExprOpt, memberOrFunc, _, _, argExprs) -> + memberCallHandler e.Range memberOrFunc + visitObjArg memberCallHandler objExprOpt + visitExprs memberCallHandler argExprs + | FSharpExprPatterns.Coerce(_, inpExpr) -> visitExpr memberCallHandler inpExpr + | FSharpExprPatterns.FastIntegerForLoop(startExpr, limitExpr, consumeExpr, _, _, _) -> + visitExpr memberCallHandler startExpr + visitExpr memberCallHandler limitExpr + visitExpr memberCallHandler consumeExpr + | FSharpExprPatterns.ILAsm(_, _, argExprs) -> visitExprs memberCallHandler argExprs + | FSharpExprPatterns.ILFieldGet(objExprOpt, _, _) -> visitObjArg memberCallHandler objExprOpt + | FSharpExprPatterns.ILFieldSet(objExprOpt, _, _, _) -> visitObjArg memberCallHandler objExprOpt + | FSharpExprPatterns.IfThenElse(guardExpr, thenExpr, elseExpr) -> + visitExpr memberCallHandler guardExpr + visitExpr memberCallHandler thenExpr + visitExpr memberCallHandler elseExpr + | FSharpExprPatterns.Lambda(_, bodyExpr) -> visitExpr memberCallHandler bodyExpr + | FSharpExprPatterns.Let((_, bindingExpr, _), bodyExpr) -> + visitExpr memberCallHandler bindingExpr + visitExpr memberCallHandler bodyExpr + | FSharpExprPatterns.LetRec(recursiveBindings, bodyExpr) -> + List.iter ((fun (_, x, _) -> x) >> visitExpr memberCallHandler) recursiveBindings + visitExpr memberCallHandler bodyExpr + | FSharpExprPatterns.NewArray(_, argExprs) -> visitExprs memberCallHandler argExprs + | FSharpExprPatterns.NewDelegate(_, delegateBodyExpr) -> visitExpr memberCallHandler delegateBodyExpr + | FSharpExprPatterns.NewObject(_, _, argExprs) -> visitExprs memberCallHandler argExprs + | FSharpExprPatterns.NewRecord(_, argExprs) -> visitExprs memberCallHandler argExprs + | FSharpExprPatterns.NewTuple(_, argExprs) -> visitExprs memberCallHandler argExprs + | FSharpExprPatterns.NewUnionCase(_, _, argExprs) -> visitExprs memberCallHandler argExprs + | FSharpExprPatterns.Quote(quotedExpr) -> visitExpr memberCallHandler quotedExpr + | FSharpExprPatterns.FSharpFieldGet(objExprOpt, _, _) -> visitObjArg memberCallHandler objExprOpt + | FSharpExprPatterns.FSharpFieldSet(objExprOpt, _, _, argExpr) -> + visitObjArg memberCallHandler objExprOpt + visitExpr memberCallHandler argExpr + | FSharpExprPatterns.Sequential(firstExpr, secondExpr) -> + visitExpr memberCallHandler firstExpr + visitExpr memberCallHandler secondExpr + | FSharpExprPatterns.TryFinally(bodyExpr, finalizeExpr, _, _) -> + visitExpr memberCallHandler bodyExpr + visitExpr memberCallHandler finalizeExpr + | FSharpExprPatterns.TryWith(bodyExpr, _, _, _, catchExpr, _, _) -> + visitExpr memberCallHandler bodyExpr + visitExpr memberCallHandler catchExpr + | FSharpExprPatterns.TupleGet(_, _, tupleExpr) -> visitExpr memberCallHandler tupleExpr + | FSharpExprPatterns.DecisionTree(decisionExpr, decisionTargets) -> + visitExpr memberCallHandler decisionExpr + List.iter (snd >> visitExpr memberCallHandler) decisionTargets + | FSharpExprPatterns.DecisionTreeSuccess(_, decisionTargetExprs) -> visitExprs memberCallHandler decisionTargetExprs + | FSharpExprPatterns.TypeLambda(_, bodyExpr) -> visitExpr memberCallHandler bodyExpr + | FSharpExprPatterns.TypeTest(_, inpExpr) -> visitExpr memberCallHandler inpExpr + | FSharpExprPatterns.UnionCaseSet(unionExpr, _, _, _, valueExpr) -> + visitExpr memberCallHandler unionExpr + visitExpr memberCallHandler valueExpr + | FSharpExprPatterns.UnionCaseGet(unionExpr, _, _, _) -> visitExpr memberCallHandler unionExpr + | FSharpExprPatterns.UnionCaseTest(unionExpr, _, _) -> visitExpr memberCallHandler unionExpr + | FSharpExprPatterns.UnionCaseTag(unionExpr, _) -> visitExpr memberCallHandler unionExpr + | FSharpExprPatterns.ObjectExpr(_, baseCallExpr, overrides, interfaceImplementations) -> + visitExpr memberCallHandler baseCallExpr + List.iter (visitObjMember memberCallHandler) overrides + List.iter (snd >> List.iter (visitObjMember memberCallHandler)) interfaceImplementations + | FSharpExprPatterns.TraitCall(_, _, _, _, _, argExprs) -> visitExprs memberCallHandler argExprs + | FSharpExprPatterns.ValueSet(_, valueExpr) -> visitExpr memberCallHandler valueExpr + | FSharpExprPatterns.WhileLoop(guardExpr, bodyExpr, _) -> + visitExpr memberCallHandler guardExpr + visitExpr memberCallHandler bodyExpr + | FSharpExprPatterns.BaseValue _ -> () + | FSharpExprPatterns.DefaultValue _ -> () + | FSharpExprPatterns.ThisValue _ -> () + | FSharpExprPatterns.Const(_, _) -> () + | FSharpExprPatterns.Value _ -> () + | _ -> () + +and visitExprs f exprs = List.iter (visitExpr f) exprs + +and visitObjArg f objOpt = Option.iter (visitExpr f) objOpt + +and visitObjMember f memb = visitExpr f memb.Body let rec visitDeclaration f d = - match d with - | FSharpImplementationFileDeclaration.Entity (e, subDecls) -> - for subDecl in subDecls do - visitDeclaration f subDecl - | FSharpImplementationFileDeclaration.MemberOrFunctionOrValue(v, vs, e) -> - visitExpr f e - | FSharpImplementationFileDeclaration.InitAction(e) -> - visitExpr f e - -let notUsed() = - let option : Option = None - option.Value + match d with + | FSharpImplementationFileDeclaration.Entity(_, subDecls) -> + for subDecl in subDecls do + visitDeclaration f subDecl + | FSharpImplementationFileDeclaration.MemberOrFunctionOrValue(_, _, e) -> visitExpr f e + | FSharpImplementationFileDeclaration.InitAction(e) -> visitExpr f e + +let notUsed () = + let option: Option = None + option.Value let logger = LogProvider.getLoggerByName "OptionAnalyzer" let info message items = let mutable log = Log.setMessage message - for (name, value) in items do + + for name, value in items do log <- log >> Log.addContextDestructured name value + logger.info log let inline (==>) x y = x, box y -[] -let optionValueAnalyzer : Analyzer = +[] +let optionValueAnalyzer: Analyzer = fun ctx -> - info "analyzing {file} for uses of Option.Value" [ "file" ==> ctx.FileName ] - let state = ResizeArray() - let handler (range: Range) (m: FSharpMemberOrFunctionOrValue) = - let rangeString = sprintf "(%d,%d)-(%d,%d)" range.Start.Line range.Start.Column range.End.Line range.End.Column - let name = String.Join(".", m.DeclaringEntity.Value.FullName, m.DisplayName) - info "checking value at {range} with name {name}" [ "range" ==> rangeString - "name" ==> name ] - if name = "Microsoft.FSharp.Core.FSharpOption`1.Value" then - info "matched at range {range}" [ "range" ==> rangeString ] - state.Add range - ctx.TypedTree.Declarations |> List.iter (visitDeclaration handler) - state - |> Seq.map (fun r -> { Type = "Option.Value analyzer" - Message = "Option.Value shouldn't be used" - Code = "OV001" - Severity = Warning - Range = r - Fixes = [] }) - |> Seq.toList + async { + info "analyzing {file} for uses of Option.Value" [ "file" ==> ctx.FileName ] + let state = ResizeArray() + + let handler (range: Range) (m: FSharpMemberOrFunctionOrValue) = + let rangeString = + sprintf "(%d,%d)-(%d,%d)" range.Start.Line range.Start.Column range.End.Line range.End.Column + + let name = String.Join(".", m.DeclaringEntity.Value.FullName, m.DisplayName) + info "checking value at {range} with name {name}" [ "range" ==> rangeString; "name" ==> name ] + + if name = "Microsoft.FSharp.Core.FSharpOption`1.Value" then + info "matched at range {range}" [ "range" ==> rangeString ] + state.Add range + + match ctx.TypedTree with + | Some tt -> tt.Declarations |> List.iter (visitDeclaration handler) + | None -> () + + return + state + |> Seq.map (fun r -> + { Type = "Option.Value analyzer" + Message = "Option.Value shouldn't be used" + Code = "OV001" + Severity = Warning + Range = r + Fixes = [] }) + |> Seq.toList + } diff --git a/test/OptionAnalyzer/paket.references b/test/OptionAnalyzer/paket.references index 3e2eb776c..313894ee0 100644 --- a/test/OptionAnalyzer/paket.references +++ b/test/OptionAnalyzer/paket.references @@ -1,2 +1 @@ -FSharp.Core -FSharp.Analyzers.Sdk +FSharp.Analyzers.Sdk \ No newline at end of file