From 1b9343bd4c9fadf38b97240c29d9c6a4bcf0042d Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Sat, 27 Apr 2024 13:45:39 -0500 Subject: [PATCH] merge main to nightly (#1273) * merge main to nightly * Update .github/workflows/build.yml * Ensure new test list can be added. (#5) * Ensure new test list can be added. * Use ParsedInput.tryPick * Just make all LSP tests sequenced. * fix "replacing unicode paragraph by newline" --------- Co-authored-by: Florian Verdonck Co-authored-by: Jimmy Byrd --- .config/dotnet-tools.json | 6 +- .devcontainer/Dockerfile | 2 +- .github/workflows/build.yml | 6 +- .paket/Paket.Restore.targets | 997 +++++++++--------- FsAutoComplete.sln | 6 - build.cmd | 2 - build.fsx | 434 ++++++++ build.sh | 4 - build/Program.fs | 35 - build/ScaffoldCodeFix.fs | 380 ------- build/ScaffoldCodeFix.fsi | 14 - build/build.fsproj | 14 - build/paket.references | 5 - docs/Creating a new code fix.md | 2 +- paket.dependencies | 22 +- paket.lock | 98 +- src/FsAutoComplete.Core/Commands.fs | 247 +++-- .../CompilerServiceInterface.fs | 23 +- src/FsAutoComplete.Core/FileSystem.fs | 15 +- src/FsAutoComplete.Core/InlayHints.fs | 8 +- src/FsAutoComplete.Core/Lexer.fs | 6 +- .../ParseAndCheckResults.fsi | 9 +- src/FsAutoComplete.Core/SignatureHelp.fs | 25 +- src/FsAutoComplete.Core/TipFormatter.fsi | 6 +- src/FsAutoComplete.Logging/FsLibLog.fs | 55 +- src/FsAutoComplete.Logging/FsOpenTelemetry.fs | 27 +- src/FsAutoComplete/CodeFixes.fs | 1 + src/FsAutoComplete/CodeFixes.fsi | 1 + .../CodeFixes/AddExplicitTypeAnnotation.fs | 9 +- .../CodeFixes/AddPrivateAccessModifier.fs | 10 +- .../CodeFixes/AddPrivateAccessModifier.fsi | 7 +- .../CodeFixes/AdjustConstant.fs | 11 +- .../CodeFixes/AdjustConstant.fsi | 2 + .../CodeFixes/ConvertPositionalDUToNamed.fs | 3 +- .../CodeFixes/ExprTypeMismatch.fs | 117 ++ .../CodeFixes/ExprTypeMismatch.fsi | 6 + .../CodeFixes/ExternalSystemDiagnostics.fs | 7 +- .../CodeFixes/RemoveUnnecessaryParentheses.fs | 111 +- src/FsAutoComplete/CommandResponse.fs | 2 +- src/FsAutoComplete/CommandResponse.fsi | 2 +- src/FsAutoComplete/LspHelpers.fs | 7 +- src/FsAutoComplete/LspHelpers.fsi | 3 + .../LspServers/AdaptiveFSharpLspServer.fs | 16 +- .../LspServers/AdaptiveServerState.fs | 30 +- src/FsAutoComplete/LspServers/Common.fs | 13 +- src/FsAutoComplete/Parser.fs | 4 +- .../CodeFixTests/ExprTypeMismatchTests.fs | 81 ++ .../CodeFixTests/Tests.fs | 62 +- test/FsAutoComplete.Tests.Lsp/CoreTests.fs | 37 + test/FsAutoComplete.Tests.Lsp/GoToTests.fs | 39 + test/FsAutoComplete.Tests.Lsp/Program.fs | 2 + .../DiagnosticFormatting.fsproj | 12 + .../TestCases/DiagnosticFormatting/Program.fs | 9 + .../TestCases/GoToCSharp/Class1.cs | 7 + .../TestCases/GoToCSharp/GoToCSharp.csproj | 7 + .../TestCases/GoToTests/External.fs | 4 +- .../TestCases/GoToTests/GoToTests.fsproj | 3 + .../TestCases/GoToTests/GoToTests.sln | 28 + 58 files changed, 1696 insertions(+), 1405 deletions(-) delete mode 100644 build.cmd create mode 100644 build.fsx delete mode 100755 build.sh delete mode 100644 build/Program.fs delete mode 100644 build/ScaffoldCodeFix.fs delete mode 100644 build/ScaffoldCodeFix.fsi delete mode 100644 build/build.fsproj delete mode 100644 build/paket.references create mode 100644 src/FsAutoComplete/CodeFixes/ExprTypeMismatch.fs create mode 100644 src/FsAutoComplete/CodeFixes/ExprTypeMismatch.fsi create mode 100644 test/FsAutoComplete.Tests.Lsp/CodeFixTests/ExprTypeMismatchTests.fs create mode 100644 test/FsAutoComplete.Tests.Lsp/TestCases/DiagnosticFormatting/DiagnosticFormatting.fsproj create mode 100644 test/FsAutoComplete.Tests.Lsp/TestCases/DiagnosticFormatting/Program.fs create mode 100644 test/FsAutoComplete.Tests.Lsp/TestCases/GoToCSharp/Class1.cs create mode 100644 test/FsAutoComplete.Tests.Lsp/TestCases/GoToCSharp/GoToCSharp.csproj create mode 100644 test/FsAutoComplete.Tests.Lsp/TestCases/GoToTests/GoToTests.sln diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index dfb834680..2dc509c37 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "paket": { - "version": "8.0.0-alpha002", + "version": "8.0.3", "commands": [ "paket" ] @@ -15,13 +15,13 @@ ] }, "fantomas": { - "version": "6.2.3", + "version": "6.3.1", "commands": [ "fantomas" ] }, "fsharp-analyzers": { - "version": "0.23.0", + "version": "0.25.0", "commands": [ "fsharp-analyzers" ] diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index d999d1003..672178010 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -3,7 +3,7 @@ # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. #------------------------------------------------------------------------------------------------------------- -ARG DOTNETCORE_VERSION=7.0.201 +ARG DOTNETCORE_VERSION=7.0.400 FROM mcr.microsoft.com/dotnet/sdk:${DOTNETCORE_VERSION} # Avoid warnings by switching to noninteractive diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f5cc6538f..20485f0cb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -96,11 +96,15 @@ jobs: run: dotnet tool restore - name: Check format - run: dotnet fantomas --check src + run: dotnet fantomas --check build.fsx src env: DOTNET_ROLL_FORWARD: LatestMajor DOTNET_ROLL_FORWARD_TO_PRERELEASE: 1 + # Ensure the scaffolding code can still add items to the existing code. +# - name: EnsureCanScaffoldCodeFix +# run: dotnet fsi build.fsx -- -p EnsureCanScaffoldCodeFix + - name: Run Build run: dotnet build -c Release env: diff --git a/.paket/Paket.Restore.targets b/.paket/Paket.Restore.targets index e230bb215..c66062b23 100644 --- a/.paket/Paket.Restore.targets +++ b/.paket/Paket.Restore.targets @@ -1,322 +1,325 @@ - - - - - - $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - - $(MSBuildVersion) - 15.0.0 - false - true - - true - $(MSBuildThisFileDirectory) - $(MSBuildThisFileDirectory)..\ - $(PaketRootPath)paket-files\paket.restore.cached - $(PaketRootPath)paket.lock - classic - proj - assembly - native - /Library/Frameworks/Mono.framework/Commands/mono - mono - - - $(PaketRootPath)paket.bootstrapper.exe - $(PaketToolsPath)paket.bootstrapper.exe - $([System.IO.Path]::GetDirectoryName("$(PaketBootStrapperExePath)"))\ - - "$(PaketBootStrapperExePath)" - $(MonoPath) --runtime=v4.0.30319 "$(PaketBootStrapperExePath)" - - - - - true - true - - - True - - - False - - $(BaseIntermediateOutputPath.TrimEnd('\').TrimEnd('\/')) - - - - - - - - - $(PaketRootPath)paket - $(PaketToolsPath)paket - - - - - - $(PaketRootPath)paket.exe - $(PaketToolsPath)paket.exe - - - - - - <_DotnetToolsJson Condition="Exists('$(PaketRootPath)/.config/dotnet-tools.json')">$([System.IO.File]::ReadAllText("$(PaketRootPath)/.config/dotnet-tools.json")) - <_ConfigContainsPaket Condition=" '$(_DotnetToolsJson)' != ''">$(_DotnetToolsJson.Contains('"paket"')) - <_ConfigContainsPaket Condition=" '$(_ConfigContainsPaket)' == ''">false - - - - - - - - - - - <_PaketCommand>dotnet paket - - - - - - $(PaketToolsPath)paket - $(PaketBootStrapperExeDir)paket - - - paket - - - - - <_PaketExeExtension>$([System.IO.Path]::GetExtension("$(PaketExePath)")) - <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(_PaketExeExtension)' == '.dll' ">dotnet "$(PaketExePath)" - <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(OS)' != 'Windows_NT' AND '$(_PaketExeExtension)' == '.exe' ">$(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" - <_PaketCommand Condition=" '$(_PaketCommand)' == '' ">"$(PaketExePath)" - - - - - - - - - - - - - - - - - - - - - true - $(NoWarn);NU1603;NU1604;NU1605;NU1608 - false - true - - - - - - - - - $([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)')) - - - - - - - $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[0].Replace(`"`, ``).Replace(` `, ``)) - $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[1].Replace(`"`, ``).Replace(` `, ``)) - - - - - %(PaketRestoreCachedKeyValue.Value) - %(PaketRestoreCachedKeyValue.Value) - - - - - true - false - true - - - + + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + $(MSBuildVersion) + 15.0.0 + false + true + + true + $(MSBuildThisFileDirectory) + $(MSBuildThisFileDirectory)..\ + $(PaketRootPath)paket-files\paket.restore.cached + $(PaketRootPath)paket.lock + classic + proj + assembly + native + /Library/Frameworks/Mono.framework/Commands/mono + mono + + + $(PaketRootPath)paket.bootstrapper.exe + $(PaketToolsPath)paket.bootstrapper.exe + $([System.IO.Path]::GetDirectoryName("$(PaketBootStrapperExePath)"))\ + + "$(PaketBootStrapperExePath)" + $(MonoPath) --runtime=v4.0.30319 "$(PaketBootStrapperExePath)" + + + + + true + true + + + True + + + False + + $(BaseIntermediateOutputPath.TrimEnd('\').TrimEnd('\/')) + + + + + + + + + $(PaketRootPath)paket + $(PaketToolsPath)paket + + + + + + $(PaketRootPath)paket.exe + $(PaketToolsPath)paket.exe + + + + + + <_DotnetToolsJson Condition="Exists('$(PaketRootPath)/.config/dotnet-tools.json')">$([System.IO.File]::ReadAllText("$(PaketRootPath)/.config/dotnet-tools.json")) + <_ConfigContainsPaket Condition=" '$(_DotnetToolsJson)' != ''">$(_DotnetToolsJson.Contains('"paket"')) + <_ConfigContainsPaket Condition=" '$(_ConfigContainsPaket)' == ''">false + + + + + + + + + + + <_PaketCommand>dotnet paket + + + + + + $(PaketToolsPath)paket + $(PaketBootStrapperExeDir)paket + + + paket + + + + + <_PaketExeExtension>$([System.IO.Path]::GetExtension("$(PaketExePath)")) + <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(_PaketExeExtension)' == '.dll' ">dotnet "$(PaketExePath)" + <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(OS)' != 'Windows_NT' AND '$(_PaketExeExtension)' == '.exe' ">$(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" + <_PaketCommand Condition=" '$(_PaketCommand)' == '' ">"$(PaketExePath)" + + + + + + + + + + + + + + + + + + + + + true + $(NoWarn);NU1603;NU1604;NU1605;NU1608 + false + true + + + + + + + + + $([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)')) + + + + + + + $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[0].Replace(`"`, ``).Replace(` `, ``)) + $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[1].Replace(`"`, ``).Replace(` `, ``)) + + + + + %(PaketRestoreCachedKeyValue.Value) + %(PaketRestoreCachedKeyValue.Value) + + + + + true + false + true + + + - - true - - - - - - - - - - - - - - - - - - - $(PaketIntermediateOutputPath)\$(MSBuildProjectFile).paket.references.cached - - $(MSBuildProjectFullPath).paket.references - - $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references - - $(MSBuildProjectDirectory)\paket.references - - false - true - true - references-file-or-cache-not-found - - - - - $([System.IO.File]::ReadAllText('$(PaketReferencesCachedFilePath)')) - $([System.IO.File]::ReadAllText('$(PaketOriginalReferencesFilePath)')) - references-file - false - - - - - false - - - - - true - target-framework '$(TargetFramework)' or '$(TargetFrameworks)' files @(PaketResolvedFilePaths) - - - - - - - - - - - false - true - - - - - - - - - - - $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',').Length) - $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0]) - $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1]) - $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[4]) - $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[5]) - $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[6]) - $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[7]) - - - %(PaketReferencesFileLinesInfo.PackageVersion) - All - runtime - $(ExcludeAssets);contentFiles - $(ExcludeAssets);build;buildMultitargeting;buildTransitive - true - true - - - - - $(PaketIntermediateOutputPath)/$(MSBuildProjectFile).paket.clitools - - - - - - - - - $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[0]) - $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[1]) - - - %(PaketCliToolFileLinesInfo.PackageVersion) - - - - + + + + + + + + + + + + + + + + $(PaketIntermediateOutputPath)\$(MSBuildProjectFile).paket.references.cached + + $(MSBuildProjectFullPath).paket.references + + $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references + + $(MSBuildProjectDirectory)\paket.references + + false + true + true + references-file-or-cache-not-found + + + + + $([System.IO.File]::ReadAllText('$(PaketReferencesCachedFilePath)')) + $([System.IO.File]::ReadAllText('$(PaketOriginalReferencesFilePath)')) + references-file + false + + + + + false + + + + + true + target-framework '$(TargetFramework)' or '$(TargetFrameworks)' files @(PaketResolvedFilePaths) + + + + + + + + + + + false + true + + + + + + + + + + + $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',').Length) + $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0]) + $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1]) + $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[4]) + $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[5]) + $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[6]) + $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[7]) + $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[8]) + + + %(PaketReferencesFileLinesInfo.PackageVersion) + All + runtime + $(ExcludeAssets);contentFiles + $(ExcludeAssets);build;buildMultitargeting;buildTransitive + %(PaketReferencesFileLinesInfo.Aliases) + true + true + + + + + + $(PaketIntermediateOutputPath)/$(MSBuildProjectFile).paket.clitools + + + + + + + + + $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[0]) + $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[1]) + + + %(PaketCliToolFileLinesInfo.PackageVersion) + + + + - - - - - false - - - - - - <_NuspecFilesNewLocation Include="$(PaketIntermediateOutputPath)\$(Configuration)\*.nuspec"/> - - - - - - $(MSBuildProjectDirectory)/$(MSBuildProjectFile) - true - false - true - false - true - false - true - false - true - false - true - $(PaketIntermediateOutputPath)\$(Configuration) - $(PaketIntermediateOutputPath) - - - - <_NuspecFiles Include="$(AdjustedNuspecOutputPath)\*.$(PackageVersion.Split(`+`)[0]).nuspec"/> - - - - - - - - - + + + + + false + + + + + + <_NuspecFilesNewLocation Include="$(PaketIntermediateOutputPath)\$(Configuration)\*.nuspec"/> + + + + + + $(MSBuildProjectDirectory)/$(MSBuildProjectFile) + true + false + true + false + true + false + true + false + true + false + true + $(PaketIntermediateOutputPath)\$(Configuration) + $(PaketIntermediateOutputPath) + + + + <_NuspecFiles Include="$(AdjustedNuspecOutputPath)\*.$(PackageVersion.Split(`+`)[0]).nuspec"/> + + + + + + + + + - - - - - - - - - + + + + + + + + + diff --git a/FsAutoComplete.sln b/FsAutoComplete.sln index 5aed2b731..880144ac3 100644 --- a/FsAutoComplete.sln +++ b/FsAutoComplete.sln @@ -25,8 +25,6 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "OptionAnalyzer", "test\Opti EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FsAutoComplete.DependencyManager.Dummy", "test\FsAutoComplete.DependencyManager.Dummy\FsAutoComplete.DependencyManager.Dummy.fsproj", "{C58701B0-D8E3-4B68-A7DE-8524C95F86C0}" EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "build", "build\build.fsproj", "{400D56D0-28C9-4210-AA30-BD688122E298}" -EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "benchmarks", "benchmarks\benchmarks.fsproj", "{0CD029D8-B39E-4CBE-A190-C84A7A811180}" EndProject Global @@ -63,10 +61,6 @@ Global {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Debug|Any CPU.Build.0 = Debug|Any CPU {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Release|Any CPU.ActiveCfg = Release|Any CPU {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Release|Any CPU.Build.0 = Release|Any CPU - {400D56D0-28C9-4210-AA30-BD688122E298}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {400D56D0-28C9-4210-AA30-BD688122E298}.Debug|Any CPU.Build.0 = Debug|Any CPU - {400D56D0-28C9-4210-AA30-BD688122E298}.Release|Any CPU.ActiveCfg = Release|Any CPU - {400D56D0-28C9-4210-AA30-BD688122E298}.Release|Any CPU.Build.0 = Release|Any CPU {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Debug|Any CPU.Build.0 = Debug|Any CPU {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/build.cmd b/build.cmd deleted file mode 100644 index 26d5d7b0c..000000000 --- a/build.cmd +++ /dev/null @@ -1,2 +0,0 @@ -dotnet tool restore -dotnet build diff --git a/build.fsx b/build.fsx new file mode 100644 index 000000000..7b91c8514 --- /dev/null +++ b/build.fsx @@ -0,0 +1,434 @@ +#r "nuget: Fun.Build, 1.1.2" +#r "nuget: Fake.Tools.Git, 6.0.0" +#r "nuget: Fake.IO.FileSystem, 6.0.0" +#r "nuget: Fantomas.Core, 6.3.1" + +open Fun.Build +open Fake.Tools + +module ScaffoldCodeFix = + open System + open System.IO + open Fake.Core + open Fake.IO.FileSystemOperators + open Fantomas.Core.SyntaxOak + + let repositoryRoot = __SOURCE_DIRECTORY__ + + let AdaptiveServerStatePath = + repositoryRoot + "src" + "FsAutoComplete" + "LspServers" + "AdaptiveServerState.fs" + + + let TestsPath = + repositoryRoot + "test" + "FsAutoComplete.Tests.Lsp" + "CodeFixTests" + "Tests.fs" + + let removeReturnCarriage (v: string) = v.Replace("\r", "") + + let mkCodeFixImplementation codeFixName = + let path = + repositoryRoot + "src" + "FsAutoComplete" + "CodeFixes" + $"{codeFixName}.fs" + + let content = + $"""module FsAutoComplete.CodeFix.%s{codeFixName} + +open FSharp.Compiler.Symbols +open FSharp.Compiler.Syntax +open FSharp.Compiler.Text +open FsToolkit.ErrorHandling +open Ionide.LanguageServerProtocol.Types +open FsAutoComplete.CodeFix.Types +open FsAutoComplete +open FsAutoComplete.LspHelpers + +// TODO: add proper title for code fix +let title = "%s{codeFixName} Codefix" + +let fix + (getParseResultsForFile: GetParseResultsForFile) + : CodeFix = + fun (codeActionParams: CodeActionParams) -> + asyncResult {{ + // Most code fixes have some general setup. + // We initially want to detect the state of the current code and whether we can propose any text edits to the user. + + let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath + // The converted LSP start position to an FCS start position. + let fcsPos = protocolPosToPos codeActionParams.Range.Start + // The syntax tree and typed tree, current line and sourceText of the current file. + let! (parseAndCheckResults:ParseAndCheckResults, line:string, sourceText:IFSACSourceText) = + getParseResultsForFile fileName fcsPos + + // The syntax tree can be an intimidating set of types to work with. + // It is a tree structure but it consists out of many different types. + // See https://fsharp.github.io/fsharp-compiler-docs/reference/fsharp-compiler-syntax.html + // It can be useful to inspect a syntax tree via a code sample using https://fsprojects.github.io/fantomas-tools/#/ast + // For example `let a b c = ()` in + // https://fsprojects.github.io/fantomas-tools/#/ast?data=N4KABGBEAmCmBmBLAdrAzpAXFSAacUiaAYmolmPAIYA2as%%2BEkAxgPZwWQ2wAuYVYAEZhmYALxgAFAEo8BSLAAeAByrJoFHgCcArrBABfIA + // Let's say we want to find the (FCS) range for identifier `a` if the user's cursor is inside the function name. + // We will query the syntax tree to verify this is the case. + let maybeFunctionNameRange = + (fcsPos, parseAndCheckResults.GetParseResults.ParseTree) + ||> ParsedInput.tryPick (fun _path node -> + match node with + // We know that `a` will be part of a `SynPat.LongIdent` + // This was visible in the online tool. + | SyntaxNode.SynPat(SynPat.LongIdent(longDotId = SynLongIdent(id = [ functionNameIdent ]))) when + // When our code fix operates on the user's code there is no way of knowing what will be inside the syntax tree. + // So we need to be careful and verify that the pattern is indeed matching the position of the cursor. + Range.rangeContainsPos functionNameIdent.idRange fcsPos + -> + Some functionNameIdent.idRange + | _ -> None) + + match maybeFunctionNameRange with + | None -> + // The cursor is not in a position we are interested in. + // This code fix should not trigger any suggestions so we return an empty list. + return [] + | Some mBindingName -> + // It turns out we are inside a let binding and we have the range of the function name. + // Just for fun, we want to detect if there is a matching typed tree symbol present for the current name. + // We could have passed the function name from the syntax visitor, instead will we grab it from the source text. + let! functionName = sourceText.GetText mBindingName + // FSharpSymbolUse is reflecting the typed tree. + // See https://fsharp.github.io/fsharp-compiler-docs/fcs/symbols.html + let symbolUse: FSharp.Compiler.CodeAnalysis.FSharpSymbolUse option = + parseAndCheckResults.GetCheckResults.GetSymbolUseAtLocation(mBindingName.EndLine, mBindingName.EndColumn, line, [ functionName ]) + + let hasFunctionDefinitionSymbol = + match symbolUse with + | None -> false + | Some symbolUse -> + // We want to verify the found symbol is indeed a definition of a function + match symbolUse.Symbol with + | :? FSharpMemberOrFunctionOrValue -> true + | _ -> false + + if not hasFunctionDefinitionSymbol then + return [] + else + // Return a list of Fix records for when the code fix is applicable. + return [ + {{ + SourceDiagnostic = None + Title = title + File = codeActionParams.TextDocument + // Based on conditional logic, you typically want to suggest a text edit to the user. + Edits = [| + {{ + // When dealing with FCS, we typically want to use the FCS flavour of range. + // However, to interact correctly with the LSP protocol, we need to return an LSP range. + Range = fcsRangeToLsp mBindingName + NewText = "Text replaced by %s{codeFixName}" + }} + |] + Kind = FixKind.Fix + }} + ] + }} +""" + + File.WriteAllText(path, removeReturnCarriage content) + Trace.tracefn $"Generated %s{Path.GetRelativePath(repositoryRoot, path)}" + + let mkCodeFixSignature codeFixName = + let path = + repositoryRoot + "src" + "FsAutoComplete" + "CodeFixes" + $"{codeFixName}.fsi" + + let content = + $"""module FsAutoComplete.CodeFix.%s{codeFixName} + +open FsAutoComplete.CodeFix.Types + +val title: string +val fix: getParseResultsForFile: GetParseResultsForFile -> CodeFix +""" + + File.WriteAllText(path, removeReturnCarriage content) + Trace.tracefn $"Generated %s{Path.GetRelativePath(repositoryRoot, path)}" + + let updateProjectFiles () = + let fsAutoCompleteProject = + repositoryRoot "src" "FsAutoComplete" "FsAutoComplete.fsproj" + + File.SetLastWriteTime(fsAutoCompleteProject, DateTime.Now) + + let fsAutoCompleteTestsLsp = + repositoryRoot + "test" + "FsAutoComplete.Tests.Lsp" + "FsAutoComplete.Tests.Lsp.fsproj" + + File.SetLastWriteTime(fsAutoCompleteTestsLsp, DateTime.Now) + + let (|IdentName|_|) (name: string) (identListNode: IdentListNode) = + match identListNode.Content with + | [ IdentifierOrDot.Ident stn ] when stn.Text = name -> Some() + | _ -> None + + let getOakFor path = + let content = File.ReadAllText path + + Fantomas.Core.CodeFormatter.ParseOakAsync(false, content) + |> Async.RunSynchronously + |> Array.head + |> fst + + let appendItemToArrayOrList item path (node: ExprArrayOrListNode) = + let lastElement = node.Elements |> List.last |> Expr.Node + let startIndent = lastElement.Range.StartColumn + let lineIdx = lastElement.Range.EndLine - 1 + let arrayEndsOnLastElement = node.Range.EndLine = lastElement.Range.EndLine + + let updatedLines = + let lines = File.ReadAllLines path + let currentLastLine = lines.[lineIdx] + let spaces = String.replicate startIndent " " + + if arrayEndsOnLastElement then + let endOfLastElement = currentLastLine.Substring(0, lastElement.Range.EndColumn) + let endOfArray = currentLastLine.Substring(lastElement.Range.EndColumn) + + lines + |> Array.updateAt lineIdx $"{endOfLastElement}\n%s{spaces}%s{item}%s{endOfArray}" + else + lines |> Array.insertAt (lineIdx + 1) $"%s{spaces}%s{item}" + + let content = String.concat "\n" updatedLines + File.WriteAllText(path, content) + Trace.tracefn $"Added \"%s{item}\" to %s{Path.GetRelativePath(repositoryRoot, path)}" + + module List = + let exactlyOneOrFail (message: string) (items: 'T list) : 'T = + if items.Length = 1 then items.Head else failwith message + + let pickOrFail (message: string) (chooser: 'T -> 'U option) (items: 'T list) : 'U = + match List.tryPick chooser items with + | None -> failwith message + | Some u -> u + + let findArrayOrListOfFail (e: Expr) = + match e with + | Expr.ArrayOrList array -> array + | e -> failwithf $"Expected to find Expr.ArrayOrList, got %A{e}" + + let findTypeWithNameOfFail (typeName: string) (mn: ModuleOrNamespaceNode) : ITypeDefn = + mn.Declarations + |> List.pickOrFail $"Expected to find ModuleDecl.TypeDefn for %s{typeName}" (function + | ModuleDecl.TypeDefn t -> + let tdn = TypeDefn.TypeDefnNode t + + match tdn.TypeName.Identifier with + | IdentName typeName -> Some tdn + | _ -> None + | _ -> None) + + let findArrayInAdaptiveFSharpLspServer () : ExprArrayOrListNode = + let oak = getOakFor AdaptiveServerStatePath + + // namespace FsAutoComplete.Lsp + let ns = + oak.ModulesOrNamespaces + |> List.exactlyOneOrFail "Expected a single namespace in Oak." + + // type AdaptiveState + let t = findTypeWithNameOfFail "AdaptiveState" ns + + // let codefixes = + let codefixesValue = + t.Members + |> List.pickOrFail "Expected to find MemberDefn.LetBinding for codefixes" (function + | MemberDefn.LetBinding bindingList -> + match bindingList.Bindings with + | bindings -> + bindings + |> List.tryPick (fun binding -> + match binding.FunctionName with + | Choice1Of2(IdentName "codefixes") -> Some binding + | _ -> None) + | _ -> None) + + let infixApp = + match codefixesValue.Expr with + | Expr.CompExprBody body -> + match List.last body.Statements with + | ComputationExpressionStatement.OtherStatement other -> + match other with + | Expr.InfixApp infixApp -> infixApp + | e -> failwithf $"Expected to find Expr.InfixApp, got %A{e}" + | ces -> failwithf $"Expected to find ComputationExpressionStatement.OtherStatement, got %A{ces}" + | e -> failwithf $"Expected to find Expr.CompExprBody, got %A{e}" + + let appWithLambda = + match infixApp.RightHandSide with + | Expr.AppWithLambda appWithLambda -> appWithLambda + | e -> failwithf $"Expected to find Expr.AppWithLambda, got %A{e}" + + let lambda = + match appWithLambda.Lambda with + | Choice1Of2 lambda -> lambda + | Choice2Of2 ml -> failwithf $"Expected to find ExprLambdaNode, got %A{ml}" + + findArrayOrListOfFail lambda.Expr + + let wireCodeFixInAdaptiveFSharpLspServer codeFixName = + try + let array = findArrayInAdaptiveFSharpLspServer () + + appendItemToArrayOrList $"%s{codeFixName}.fix tryGetParseAndCheckResultsForFile" AdaptiveServerStatePath array + with ex -> + Trace.traceException ex + + Trace.traceError + $"Unable to find array of codefixes in %s{AdaptiveServerStatePath}.\nDid the code structure change?" + + + let mkCodeFixTests codeFixName = + let path = + repositoryRoot + "test" + "FsAutoComplete.Tests.Lsp" + "CodeFixTests" + $"%s{codeFixName}Tests.fs" + + let contents = + $"module private FsAutoComplete.Tests.CodeFixTests.%s{codeFixName}Tests + +open Expecto +open Helpers +open Utils.ServerTests +open Utils.CursorbasedTests +open FsAutoComplete.CodeFix + +let tests state = + serverTestList (nameof %s{codeFixName}) state defaultConfigDto None (fun server -> + [ let selectCodeFix = CodeFix.withTitle %s{codeFixName}.title + + ftestCaseAsync \"first unit test for %s{codeFixName}\" + <| CodeFix.check + server + \"let a$0 b c = ()\" + Diagnostics.acceptAll + selectCodeFix + \"let Text replaced by %s{codeFixName} b c = ()\" + ]) +" + + File.WriteAllText(path, removeReturnCarriage contents) + Trace.tracefn $"Generated %s{Path.GetRelativePath(repositoryRoot, path)}" + + let findListInTests () = + let oak = getOakFor TestsPath + // module FsAutoComplete.Tests.CodeFixTests.Tests + let testsModule = + oak.ModulesOrNamespaces + |> List.exactlyOneOrFail "Expected a single module in Oak." + + // let tests state = + let testBinding = + testsModule.Declarations + |> List.pickOrFail "Expected to find ModuleDecl.TopLevelBinding for tests" (function + | ModuleDecl.TopLevelBinding binding -> + match binding.FunctionName with + | Choice1Of2(IdentName "tests") -> Some binding + | _ -> None + | _ -> None) + + let appNode = + match testBinding.Expr with + | Expr.InfixApp infixApp -> + match infixApp.RightHandSide with + | Expr.App appNode -> appNode + | e -> failwithf $"Expected Expr.App, got %A{e}" + | e -> failwithf $"Expected Expr.InfixApp, got %A{e}" + + findArrayOrListOfFail (List.last appNode.Arguments) + + let wireCodeFixTests codeFixName = + try + let list = findListInTests () + appendItemToArrayOrList $"%s{codeFixName}Tests.tests state" TestsPath list + with ex -> + Trace.traceException ex + Trace.traceError $"Unable to find array of tests in %s{TestsPath}.\nDid the code structure change?" + + let scaffold (codeFixName: string) : unit = + // generate files in src/CodeFixes/ + mkCodeFixImplementation codeFixName + mkCodeFixSignature codeFixName + + // Wire up codefix to LSP servers + wireCodeFixInAdaptiveFSharpLspServer codeFixName + + // Add test file + mkCodeFixTests codeFixName + + // Wire up tests in test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs + wireCodeFixTests codeFixName + + updateProjectFiles () + Trace.tracefn $"Scaffolding %s{codeFixName} complete!" + + let ensureScaffoldStillWorks () = + findArrayInAdaptiveFSharpLspServer () |> ignore + findListInTests () |> ignore + +pipeline "EnsureRepoConfig" { + description "Configure custom git hooks, currently only used to ensure that code is formatted before pushing" + workingDir __SOURCE_DIRECTORY__ + stage "Git" { run (fun _ -> Git.CommandHelper.gitCommand "" "config core.hooksPath .githooks") } + runIfOnlySpecified true +} + +pipeline "ScaffoldCodeFix" { + description "Scaffold a new code fix." + workingDir __SOURCE_DIRECTORY__ + + stage "Scaffold" { + run (fun ctx -> + let codeFixName = ctx.GetAllCmdArgs() |> List.tryLast + + match codeFixName with + | None -> printfn "Usage: dotnet fsi build.fsx -- -p ScaffoldCodeFix " + | Some codeFixName -> ScaffoldCodeFix.scaffold codeFixName) + } + + runIfOnlySpecified true +} + +pipeline "EnsureCanScaffoldCodeFix" { + description "Ensure the ScaffoldCodeFix pipeline can still be executed." + workingDir __SOURCE_DIRECTORY__ + stage "Ensure" { run (fun _ -> ScaffoldCodeFix.ensureScaffoldStillWorks ()) } + runIfOnlySpecified true +} + +pipeline "Build" { + description "Default build pipeline" + workingDir __SOURCE_DIRECTORY__ + + stage "Build" { + run "dotnet tool restore" + run "dotnet build" + } + + runIfOnlySpecified false +} + +tryPrintPipelineCommandHelp () diff --git a/build.sh b/build.sh deleted file mode 100755 index a0a6f01cf..000000000 --- a/build.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -dotnet tool restore -dotnet build diff --git a/build/Program.fs b/build/Program.fs deleted file mode 100644 index 79a864066..000000000 --- a/build/Program.fs +++ /dev/null @@ -1,35 +0,0 @@ -open Fake.Core -open Fake.IO -open Fake.Tools - -System.Environment.CurrentDirectory <- (Path.combine __SOURCE_DIRECTORY__ "..") - -let init args = - let execContext = Context.FakeExecutionContext.Create false "build.fsx" args - Context.setExecutionContext (Context.RuntimeContext.Fake execContext) - Target.initEnvironment () - - Target.create "EnsureRepoConfig" (fun _ -> - // Configure custom git hooks - // * Currently only used to ensure that code is formatted before pushing - Git.CommandHelper.gitCommand "" "config core.hooksPath .githooks") - - Target.create "ScaffoldCodeFix" (fun ctx -> - let codeFixName = ctx.Context.Arguments |> List.tryHead - - match codeFixName with - | None -> failwith "Usage: dotnet run --project ./build/build.fsproj -- -t ScaffoldCodeFix " - | Some codeFixName -> ScaffoldCodeFix.scaffold codeFixName) - - Target.create "EnsureCanScaffoldCodeFix" (fun _ -> ScaffoldCodeFix.ensureScaffoldStillWorks ()) - -[] -let main args = - init (args |> List.ofArray) - - try - Target.runOrDefaultWithArguments "EnsureCanScaffoldCodeFix" - 0 - with e -> - printfn "%A" e - 1 diff --git a/build/ScaffoldCodeFix.fs b/build/ScaffoldCodeFix.fs deleted file mode 100644 index 0e3d1cb59..000000000 --- a/build/ScaffoldCodeFix.fs +++ /dev/null @@ -1,380 +0,0 @@ -module ScaffoldCodeFix - -open System -open System.IO -open Fake.Core -open Fake.IO.FileSystemOperators -open Fantomas.Core.SyntaxOak - -let repositoryRoot = __SOURCE_DIRECTORY__ ".." - -let AdaptiveServerStatePath = - repositoryRoot - "src" - "FsAutoComplete" - "LspServers" - "AdaptiveServerState.fs" - - -let TestsPath = - repositoryRoot - "test" - "FsAutoComplete.Tests.Lsp" - "CodeFixTests" - "Tests.fs" - -let removeReturnCarriage (v: string) = v.Replace("\r", "") - -let mkCodeFixImplementation codeFixName = - let path = - repositoryRoot - "src" - "FsAutoComplete" - "CodeFixes" - $"{codeFixName}.fs" - - let content = - $"""module FsAutoComplete.CodeFix.%s{codeFixName} - -open FSharp.Compiler.Symbols -open FSharp.Compiler.Syntax -open FSharp.Compiler.Text -open FsToolkit.ErrorHandling -open Ionide.LanguageServerProtocol.Types -open FsAutoComplete.CodeFix.Types -open FsAutoComplete -open FsAutoComplete.LspHelpers - -// TODO: add proper title for code fix -let title = "%s{codeFixName} Codefix" - -let fix - (getParseResultsForFile: GetParseResultsForFile) - : CodeFix = - fun (codeActionParams: CodeActionParams) -> - asyncResult {{ - // Most code fixes have some general setup. - // We initially want to detect the state of the current code and whether we can propose any text edits to the user. - - let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath - // The converted LSP start position to an FCS start position. - let fcsPos = protocolPosToPos codeActionParams.Range.Start - // The syntax tree and typed tree, current line and sourceText of the current file. - let! (parseAndCheckResults:ParseAndCheckResults, line:string, sourceText:IFSACSourceText) = - getParseResultsForFile fileName fcsPos - - // The syntax tree can be an intimidating set of types to work with. - // It is a tree structure but it consists out of many different types. - // See https://fsharp.github.io/fsharp-compiler-docs/reference/fsharp-compiler-syntax.html - // It can be useful to inspect a syntax tree via a code sample using https://fsprojects.github.io/fantomas-tools/#/ast - // For example `let a b c = ()` in - // https://fsprojects.github.io/fantomas-tools/#/ast?data=N4KABGBEAmCmBmBLAdrAzpAXFSAacUiaAYmolmPAIYA2as%%2BEkAxgPZwWQ2wAuYVYAEZhmYALxgAFAEo8BSLAAeAByrJoFHgCcArrBABfIA - // Let's say we want to find the (FCS) range for identifier `a` if the user's cursor is inside the function name. - // We will query the syntax tree to verify this is the case. - let maybeFunctionNameRange = - (fcsPos, parseAndCheckResults.GetParseResults.ParseTree) - ||> ParsedInput.tryPick (fun _path node -> - match node with - // We know that `a` will be part of a `SynPat.LongIdent` - // This was visible in the online tool. - | SyntaxNode.SynPat(SynPat.LongIdent(longDotId = SynLongIdent(id = [ functionNameIdent ]))) when - // When our code fix operates on the user's code there is no way of knowing what will be inside the syntax tree. - // So we need to be careful and verify that the pattern is indeed matching the position of the cursor. - Range.rangeContainsPos functionNameIdent.idRange fcsPos - -> - Some functionNameIdent.idRange - | _ -> None) - - match maybeFunctionNameRange with - | None -> - // The cursor is not in a position we are interested in. - // This code fix should not trigger any suggestions so we return an empty list. - return [] - | Some mBindingName -> - // It turns out we are inside a let binding and we have the range of the function name. - // Just for fun, we want to detect if there is a matching typed tree symbol present for the current name. - // We could have passed the function name from the syntax visitor, instead will we grab it from the source text. - let! functionName = sourceText.GetText mBindingName - // FSharpSymbolUse is reflecting the typed tree. - // See https://fsharp.github.io/fsharp-compiler-docs/fcs/symbols.html - let symbolUse: FSharp.Compiler.CodeAnalysis.FSharpSymbolUse option = - parseAndCheckResults.GetCheckResults.GetSymbolUseAtLocation(mBindingName.EndLine, mBindingName.EndColumn, line, [ functionName ]) - - let hasFunctionDefinitionSymbol = - match symbolUse with - | None -> false - | Some symbolUse -> - // We want to verify the found symbol is indeed a definition of a function - match symbolUse.Symbol with - | :? FSharpMemberOrFunctionOrValue -> true - | _ -> false - - if not hasFunctionDefinitionSymbol then - return [] - else - // Return a list of Fix records for when the code fix is applicable. - return [ - {{ - SourceDiagnostic = None - Title = title - File = codeActionParams.TextDocument - // Based on conditional logic, you typically want to suggest a text edit to the user. - Edits = [| - {{ - // When dealing with FCS, we typically want to use the FCS flavour of range. - // However, to interact correctly with the LSP protocol, we need to return an LSP range. - Range = fcsRangeToLsp mBindingName - NewText = "Text replaced by %s{codeFixName}" - }} - |] - Kind = FixKind.Fix - }} - ] - }} -""" - - File.WriteAllText(path, removeReturnCarriage content) - Trace.tracefn $"Generated %s{Path.GetRelativePath(repositoryRoot, path)}" - -let mkCodeFixSignature codeFixName = - let path = - repositoryRoot - "src" - "FsAutoComplete" - "CodeFixes" - $"{codeFixName}.fsi" - - let content = - $"""module FsAutoComplete.CodeFix.%s{codeFixName} - -open FsAutoComplete.CodeFix.Types - -val title: string -val fix: getParseResultsForFile: GetParseResultsForFile -> CodeFix -""" - - File.WriteAllText(path, removeReturnCarriage content) - Trace.tracefn $"Generated %s{Path.GetRelativePath(repositoryRoot, path)}" - -let updateProjectFiles () = - let fsAutoCompleteProject = - repositoryRoot "src" "FsAutoComplete" "FsAutoComplete.fsproj" - - File.SetLastWriteTime(fsAutoCompleteProject, DateTime.Now) - - let fsAutoCompleteTestsLsp = - repositoryRoot - "test" - "FsAutoComplete.Tests.Lsp" - "FsAutoComplete.Tests.Lsp.fsproj" - - File.SetLastWriteTime(fsAutoCompleteTestsLsp, DateTime.Now) - -let (|IdentName|_|) (name: string) (identListNode: IdentListNode) = - match identListNode.Content with - | [ IdentifierOrDot.Ident stn ] when stn.Text = name -> Some() - | _ -> None - -let getOakFor path = - let content = File.ReadAllText path - - Fantomas.Core.CodeFormatter.ParseOakAsync(false, content) - |> Async.RunSynchronously - |> Array.head - |> fst - -let appendItemToArrayOrList item path (node: ExprArrayOrListNode) = - let lastElement = node.Elements |> List.last |> Expr.Node - let startIndent = lastElement.Range.StartColumn - let lineIdx = lastElement.Range.EndLine - 1 - let arrayEndsOnLastElement = node.Range.EndLine = lastElement.Range.EndLine - - let updatedLines = - let lines = File.ReadAllLines path - let currentLastLine = lines.[lineIdx] - let spaces = String.replicate startIndent " " - - if arrayEndsOnLastElement then - let endOfLastElement = currentLastLine.Substring(0, lastElement.Range.EndColumn) - let endOfArray = currentLastLine.Substring(lastElement.Range.EndColumn) - - lines - |> Array.updateAt lineIdx $"{endOfLastElement}\n%s{spaces}%s{item}%s{endOfArray}" - else - lines |> Array.insertAt (lineIdx + 1) $"%s{spaces}%s{item}" - - let content = String.concat "\n" updatedLines - File.WriteAllText(path, content) - Trace.tracefn $"Added \"%s{item}\" to %s{Path.GetRelativePath(repositoryRoot, path)}" - -module List = - let exactlyOneOrFail (message: string) (items: 'T list) : 'T = - if items.Length = 1 then items.Head else failwith message - - let pickOrFail (message: string) (chooser: 'T -> 'U option) (items: 'T list) : 'U = - match List.tryPick chooser items with - | None -> failwith message - | Some u -> u - -let findArrayOrListOfFail (e: Expr) = - match e with - | Expr.ArrayOrList array -> array - | e -> failwithf $"Expected to find Expr.ArrayOrList, got %A{e}" - -let findTypeWithNameOfFail (typeName: string) (mn: ModuleOrNamespaceNode) : ITypeDefn = - mn.Declarations - |> List.pickOrFail $"Expected to find ModuleDecl.TypeDefn for %s{typeName}" (function - | ModuleDecl.TypeDefn t -> - let tdn = TypeDefn.TypeDefnNode t - - match tdn.TypeName.Identifier with - | IdentName typeName -> Some tdn - | _ -> None - | _ -> None) - -let findArrayInAdaptiveFSharpLspServer () : ExprArrayOrListNode = - let oak = getOakFor AdaptiveServerStatePath - - // namespace FsAutoComplete.Lsp - let ns = - oak.ModulesOrNamespaces - |> List.exactlyOneOrFail "Expected a single namespace in Oak." - - // type AdaptiveState - let t = findTypeWithNameOfFail "AdaptiveState" ns - - // let codefixes = - let codefixesValue = - t.Members - |> List.pickOrFail "Expected to find MemberDefn.LetBinding for codefixes" (function - | MemberDefn.LetBinding bindingList -> - match bindingList.Bindings with - | bindings -> - bindings - |> List.tryPick (fun binding -> - match binding.FunctionName with - | Choice1Of2(IdentName "codefixes") -> Some binding - | _ -> None) - | _ -> None) - - let infixApp = - match codefixesValue.Expr with - | Expr.CompExprBody body -> - match List.last body.Statements with - | ComputationExpressionStatement.OtherStatement other -> - match other with - | Expr.InfixApp infixApp -> infixApp - | e -> failwithf $"Expected to find Expr.InfixApp, got %A{e}" - | ces -> failwithf $"Expected to find ComputationExpressionStatement.OtherStatement, got %A{ces}" - | e -> failwithf $"Expected to find Expr.CompExprBody, got %A{e}" - - let appWithLambda = - match infixApp.RightHandSide with - | Expr.AppWithLambda appWithLambda -> appWithLambda - | e -> failwithf $"Expected to find Expr.AppWithLambda, got %A{e}" - - let lambda = - match appWithLambda.Lambda with - | Choice1Of2 lambda -> lambda - | Choice2Of2 ml -> failwithf $"Expected to find ExprLambdaNode, got %A{ml}" - - findArrayOrListOfFail lambda.Expr - -let wireCodeFixInAdaptiveFSharpLspServer codeFixName = - try - let array = findArrayInAdaptiveFSharpLspServer () - - appendItemToArrayOrList $"%s{codeFixName}.fix tryGetParseResultsForFile" AdaptiveServerStatePath array - with ex -> - Trace.traceException ex - - Trace.traceError - $"Unable to find array of codefixes in %s{AdaptiveServerStatePath}.\nDid the code structure change?" - - -let mkCodeFixTests codeFixName = - let path = - repositoryRoot - "test" - "FsAutoComplete.Tests.Lsp" - "CodeFixTests" - $"%s{codeFixName}Tests.fs" - - let contents = - $"module private FsAutoComplete.Tests.CodeFixTests.%s{codeFixName}Tests - -open Expecto -open Helpers -open Utils.ServerTests -open Utils.CursorbasedTests -open FsAutoComplete.CodeFix - -let tests state = - serverTestList (nameof %s{codeFixName}) state defaultConfigDto None (fun server -> - [ let selectCodeFix = CodeFix.withTitle %s{codeFixName}.title - - ftestCaseAsync \"first unit test for %s{codeFixName}\" - <| CodeFix.check - server - \"let a$0 b c = ()\" - Diagnostics.acceptAll - selectCodeFix - \"let Text replaced by %s{codeFixName} b c = ()\" - ]) -" - - File.WriteAllText(path, removeReturnCarriage contents) - Trace.tracefn $"Generated %s{Path.GetRelativePath(repositoryRoot, path)}" - -let findListInTests () = - let oak = getOakFor TestsPath - // module FsAutoComplete.Tests.CodeFixTests.Tests - let testsModule = - oak.ModulesOrNamespaces - |> List.exactlyOneOrFail "Expected a single module in Oak." - - // let tests state = - let testBinding = - testsModule.Declarations - |> List.pickOrFail "Expected to find ModuleDecl.TopLevelBinding for tests" (function - | ModuleDecl.TopLevelBinding binding -> - match binding.FunctionName with - | Choice1Of2(IdentName "tests") -> Some binding - | _ -> None - | _ -> None) - - let appNode = - match testBinding.Expr with - | Expr.App appNode -> appNode - | e -> failwithf $"Expected Expr.App, got %A{e}" - - findArrayOrListOfFail (List.last appNode.Arguments) - -let wireCodeFixTests codeFixName = - try - let list = findListInTests () - appendItemToArrayOrList $"%s{codeFixName}Tests.tests state" TestsPath list - with ex -> - Trace.traceException ex - Trace.traceError $"Unable to find array of tests in %s{TestsPath}.\nDid the code structure change?" - -let scaffold (codeFixName: string) : unit = - // generate files in src/CodeFixes/ - mkCodeFixImplementation codeFixName - mkCodeFixSignature codeFixName - - // Wire up codefix to LSP servers - wireCodeFixInAdaptiveFSharpLspServer codeFixName - - // Add test file - mkCodeFixTests codeFixName - - // Wire up tests in test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs - wireCodeFixTests codeFixName - - updateProjectFiles () - Trace.tracefn $"Scaffolding %s{codeFixName} complete!" - -let ensureScaffoldStillWorks () = - findArrayInAdaptiveFSharpLspServer () |> ignore - findListInTests () |> ignore diff --git a/build/ScaffoldCodeFix.fsi b/build/ScaffoldCodeFix.fsi deleted file mode 100644 index 5841a7d55..000000000 --- a/build/ScaffoldCodeFix.fsi +++ /dev/null @@ -1,14 +0,0 @@ -module ScaffoldCodeFix - -/// Scaffold a new CodeFix by: -/// - Generating the implementation and signature files. -/// - Wire up the codefix AdaptiveFSharpLspServer.fs -/// - Generate a tests file with a focused test. -/// - Wire up the tests file. -/// - Update the last write time the project files. -val scaffold: codeFixName: string -> unit - -/// Verifies that the code fix scaffold target can still wire up a new codefix to the existing list. -/// Throws when any expected AST nodes can no longer be found. -/// If this code throws, you may need to revisit ScaffoldCodeFix.fs to tweak any recent changes. -val ensureScaffoldStillWorks: unit -> unit diff --git a/build/build.fsproj b/build/build.fsproj deleted file mode 100644 index 071fbd2be..000000000 --- a/build/build.fsproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - Exe - net7.0 - false - - - - - - - - diff --git a/build/paket.references b/build/paket.references deleted file mode 100644 index adbe00212..000000000 --- a/build/paket.references +++ /dev/null @@ -1,5 +0,0 @@ -group Build -Fake.Core.Target -Fake.IO.FileSystem -Fake.Tools.Git -Fantomas.Core diff --git a/docs/Creating a new code fix.md b/docs/Creating a new code fix.md index 9e2844286..88d38b032 100644 --- a/docs/Creating a new code fix.md +++ b/docs/Creating a new code fix.md @@ -15,7 +15,7 @@ To introduce a new code fix within the context of FSAutocomplete, there are seve To streamline the process of creating a new code fix, a convenient `FAKE` target has been provided. By executing the following command: ```bash -dotnet run --project ./build/build.fsproj -- -t ScaffoldCodeFix YourCodeFixName +dotnet fsi build.fsx -- -p ScaffoldCodeFix YourCodeFixName ``` The above command accomplishes the following tasks: diff --git a/paket.dependencies b/paket.dependencies index ae5eed6ee..efa574d83 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -14,12 +14,12 @@ lowest_matching: true nuget BenchmarkDotNet 0.13.5 nuget Fantomas.Client >= 0.9 nuget FSharp.Compiler.Service >= 43.8.300-preview.24205.4 -nuget Ionide.Analyzers 0.7.0 +nuget Ionide.Analyzers 0.10.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 -nuget Ionide.ProjInfo.Sln >= 0.62.0 +nuget Ionide.ProjInfo >= 0.64.0 +nuget Ionide.ProjInfo.FCS >= 0.64.0 +nuget Ionide.ProjInfo.ProjectSystem >= 0.64.0 +nuget Ionide.ProjInfo.Sln >= 0.64.0 nuget Microsoft.Build >= 17.2 copy_local:false nuget Microsoft.Build.Framework >= 17.4 copy_local:false nuget Microsoft.Build.Utilities.Core >= 17.4 copy_local:false @@ -61,15 +61,3 @@ 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 - source https://api.nuget.org/v3/index.json - storage: none - - framework: net7.0 - - nuget Fake.Core.Target - nuget Fake.IO.FileSystem - nuget Fake.Tools.Git - nuget Fantomas.Core 6.2.0 diff --git a/paket.lock b/paket.lock index 8b2c76528..543cabeb1 100644 --- a/paket.lock +++ b/paket.lock @@ -116,31 +116,31 @@ NUGET Microsoft.Win32.Registry (>= 5.0) System.Collections.Immutable (>= 5.0) System.Reflection.Metadata (>= 5.0) - Ionide.Analyzers (0.7) + Ionide.Analyzers (0.10) Ionide.KeepAChangelog.Tasks (0.1.8) - copy_local: true Ionide.LanguageServerProtocol (0.4.20) FSharp.Core (>= 6.0) Newtonsoft.Json (>= 13.0.1) StreamJsonRpc (>= 2.16.36) - Ionide.ProjInfo (0.63) + Ionide.ProjInfo (0.64) FSharp.Core (>= 7.0.400) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) - Ionide.ProjInfo.Sln (>= 0.63) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) + Ionide.ProjInfo.Sln (>= 0.64) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) Microsoft.Build (>= 17.2) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) Microsoft.Build.Framework (>= 17.2) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) SemanticVersioning (>= 2.0.2) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) - Ionide.ProjInfo.FCS (0.63) + Ionide.ProjInfo.FCS (0.64) FSharp.Compiler.Service (>= 43.7.400) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) FSharp.Core (>= 7.0.400) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) - Ionide.ProjInfo (>= 0.63) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) - Ionide.ProjInfo.ProjectSystem (0.63) + Ionide.ProjInfo (>= 0.64) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) + Ionide.ProjInfo.ProjectSystem (0.64) FSharp.Compiler.Service (>= 43.7.400) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) FSharp.Control.Reactive (>= 5.0.5) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) FSharp.Core (>= 7.0.400) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) - Ionide.ProjInfo (>= 0.63) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) - Ionide.ProjInfo.FCS (>= 0.63) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) - Ionide.ProjInfo.Sln (>= 0.63) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) + Ionide.ProjInfo (>= 0.64) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) + Ionide.ProjInfo.FCS (>= 0.64) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) + Ionide.ProjInfo.Sln (>= 0.64) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) Newtonsoft.Json (>= 13.0.1) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) - Ionide.ProjInfo.Sln (0.63) + Ionide.ProjInfo.Sln (0.64) LinkDotNet.StringBuilder (1.18) MessagePack (2.5.108) MessagePack.Annotations (>= 2.5.108) @@ -748,6 +748,7 @@ NUGET Expecto (>= 10.0 < 11.0) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) 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.300-preview.24205.4) FSharp.Core (8.0.300-beta.24205.4) @@ -759,80 +760,3 @@ NUGET System.Reflection.Metadata (>= 7.0) System.Runtime.CompilerServices.Unsafe (>= 6.0) FSharp.Core (8.0.300-beta.24205.4) - -GROUP Build -STORAGE: NONE -RESTRICTION: == net7.0 -NUGET - remote: https://api.nuget.org/v3/index.json - Fake.Core.CommandLineParsing (6.0) - FParsec (>= 1.1.1) - FSharp.Core (>= 6.0.3) - Fake.Core.Context (6.0) - FSharp.Core (>= 6.0.3) - Fake.Core.Environment (6.0) - FSharp.Core (>= 6.0.3) - Fake.Core.FakeVar (6.0) - Fake.Core.Context (>= 6.0) - FSharp.Core (>= 6.0.3) - Fake.Core.Process (6.0) - Fake.Core.Environment (>= 6.0) - Fake.Core.FakeVar (>= 6.0) - Fake.Core.String (>= 6.0) - Fake.Core.Trace (>= 6.0) - Fake.IO.FileSystem (>= 6.0) - FSharp.Core (>= 6.0.3) - System.Collections.Immutable (>= 6.0) - Fake.Core.SemVer (6.0) - FSharp.Core (>= 6.0.3) - Fake.Core.String (6.0) - FSharp.Core (>= 6.0.3) - Fake.Core.Target (6.0) - Fake.Core.CommandLineParsing (>= 6.0) - Fake.Core.Context (>= 6.0) - Fake.Core.Environment (>= 6.0) - Fake.Core.FakeVar (>= 6.0) - Fake.Core.Process (>= 6.0) - Fake.Core.String (>= 6.0) - Fake.Core.Trace (>= 6.0) - FSharp.Control.Reactive (>= 5.0.2) - FSharp.Core (>= 6.0.3) - Fake.Core.Trace (6.0) - Fake.Core.Environment (>= 6.0) - Fake.Core.FakeVar (>= 6.0) - FSharp.Core (>= 6.0.3) - Fake.IO.FileSystem (6.0) - Fake.Core.String (>= 6.0) - Fake.Core.Trace (>= 6.0) - FSharp.Core (>= 6.0.3) - Fake.Tools.Git (6.0) - Fake.Core.Environment (>= 6.0) - Fake.Core.Process (>= 6.0) - Fake.Core.SemVer (>= 6.0) - Fake.Core.String (>= 6.0) - Fake.Core.Trace (>= 6.0) - Fake.IO.FileSystem (>= 6.0) - FSharp.Core (>= 6.0.3) - Fantomas.Core (6.2) - Fantomas.FCS (>= 6.2) - FSharp.Core (>= 6.0.1) - Fantomas.FCS (6.2) - FSharp.Core (>= 6.0.1) - System.Diagnostics.DiagnosticSource (>= 7.0) - System.Memory (>= 4.5.5) - System.Runtime (>= 4.3.1) - FParsec (1.1.1) - FSharp.Core (>= 4.3.4) - FSharp.Control.Reactive (5.0.5) - FSharp.Core (>= 4.7.2) - System.Reactive (>= 5.0 < 6.0) - FSharp.Core (6.0.5) - Microsoft.NETCore.Platforms (7.0.4) - Microsoft.NETCore.Targets (5.0) - System.Collections.Immutable (7.0) - System.Diagnostics.DiagnosticSource (7.0.2) - System.Memory (4.5.5) - System.Reactive (5.0) - System.Runtime (4.3.1) - Microsoft.NETCore.Platforms (>= 1.1.1) - Microsoft.NETCore.Targets (>= 1.1.3) diff --git a/src/FsAutoComplete.Core/Commands.fs b/src/FsAutoComplete.Core/Commands.fs index 543b4447b..14e29363a 100644 --- a/src/FsAutoComplete.Core/Commands.fs +++ b/src/FsAutoComplete.Core/Commands.fs @@ -1265,103 +1265,157 @@ type Commands() = /// calculates the required indent and gives the position to insert the text. static member GenerateXmlDocumentation(tyRes: ParseAndCheckResults, triggerPosition: Position, lineStr: LineStr) = asyncResult { + let tryGetFirstAttributeLine (synAttributes: SynAttributes) = + synAttributes + |> List.collect (fun a -> a.Attributes) + |> function + | [] -> None + | attributes -> + attributes + |> Seq.minBy (fun a -> a.Range.StartLine) + |> fun attr -> Some attr.Range.StartLine + let longIdentContainsPos (longIdent: LongIdent) (pos: FSharp.Compiler.Text.pos) = - longIdent |> List.exists (fun i -> rangeContainsPos i.idRange pos) + longIdent + |> List.tryFind (fun i -> rangeContainsPos i.idRange pos) + |> Option.isSome + + let isLowerAstElemWithEmptyPreXmlDoc input pos : Option> = + SyntaxTraversal.Traverse( + pos, + input, + { new SyntaxVisitorBase<_>() with + member _.VisitBinding(_, defaultTraverse, synBinding) = + match synBinding with + | SynBinding(attributes = attributes; xmlDoc = xmlDoc; valData = valData) as s when + rangeContainsPos s.RangeOfBindingWithoutRhs pos && xmlDoc.IsEmpty + -> + match valData with + | SynValData(memberFlags = Some({ MemberKind = SynMemberKind.PropertyGet })) + | SynValData(memberFlags = Some({ MemberKind = SynMemberKind.PropertySet })) + | SynValData(memberFlags = Some({ MemberKind = SynMemberKind.PropertyGetSet })) -> None + | _ -> Some(false, tryGetFirstAttributeLine attributes) + | _ -> defaultTraverse synBinding + + member _.VisitComponentInfo(_, synComponentInfo) = + match synComponentInfo with + | SynComponentInfo(attributes = attributes; longId = longId; xmlDoc = xmlDoc) when + longIdentContainsPos longId pos && xmlDoc.IsEmpty + -> + Some(false, tryGetFirstAttributeLine attributes) + | _ -> None + + member _.VisitRecordDefn(_, fields, _) = + let isInLine c = + match c with + | SynField(attributes = attributes; xmlDoc = xmlDoc; idOpt = Some ident) when + rangeContainsPos ident.idRange pos && xmlDoc.IsEmpty + -> + Some(false, tryGetFirstAttributeLine attributes) + | _ -> None + + fields |> List.tryPick isInLine + + member _.VisitUnionDefn(_, cases, _) = + let isInLine c = + match c with + | SynUnionCase(attributes = attributes; xmlDoc = xmlDoc; ident = (SynIdent(ident = ident))) when + rangeContainsPos ident.idRange pos && xmlDoc.IsEmpty + -> + Some(false, tryGetFirstAttributeLine attributes) + | _ -> None + + cases |> List.tryPick isInLine + + member _.VisitEnumDefn(_, cases, _) = + let isInLine b = + match b with + | SynEnumCase(attributes = attributes; xmlDoc = xmlDoc; ident = (SynIdent(ident = ident))) when + rangeContainsPos ident.idRange pos && xmlDoc.IsEmpty + -> + Some(false, tryGetFirstAttributeLine attributes) + | _ -> None + + cases |> List.tryPick isInLine + + member _.VisitLetOrUse(_, _, defaultTraverse, bindings, _) = + let isInLine b = + match b with + | SynBinding(attributes = attributes; xmlDoc = xmlDoc) as s when + rangeContainsPos s.RangeOfBindingWithoutRhs pos && xmlDoc.IsEmpty + -> + Some(false, tryGetFirstAttributeLine attributes) + | _ -> defaultTraverse b + + bindings |> List.tryPick isInLine + + member _.VisitExpr(_, _, defaultTraverse, expr) = defaultTraverse expr } // needed for nested let bindings + ) + + let isModuleOrNamespaceOrAutoPropertyWithEmptyPreXmlDoc input pos : Option> = + SyntaxTraversal.Traverse( + pos, + input, + { new SyntaxVisitorBase<_>() with + + member _.VisitModuleOrNamespace(_, synModuleOrNamespace) = + match synModuleOrNamespace with + | SynModuleOrNamespace(attribs = attributes; longId = longId; xmlDoc = xmlDoc) when + longIdentContainsPos longId pos && xmlDoc.IsEmpty + -> + Some(false, tryGetFirstAttributeLine attributes) + | SynModuleOrNamespace(decls = decls) -> + + let rec findNested decls = + decls + |> List.tryPick (fun d -> + match d with + | SynModuleDecl.NestedModule(moduleInfo = moduleInfo; decls = decls) -> + match moduleInfo with + | SynComponentInfo(attributes = attributes; longId = longId; xmlDoc = xmlDoc) when + longIdentContainsPos longId pos && xmlDoc.IsEmpty + -> + Some(false, tryGetFirstAttributeLine attributes) + | _ -> findNested decls + | SynModuleDecl.Types(typeDefns = typeDefns) -> + typeDefns + |> List.tryPick (fun td -> + match td with + | SynTypeDefn(typeRepr = SynTypeDefnRepr.ObjectModel(_, members, _)) -> + members + |> List.tryPick (fun m -> + match m with + | SynMemberDefn.AutoProperty(attributes = attributes; ident = ident; xmlDoc = xmlDoc) when + rangeContainsPos ident.idRange pos && xmlDoc.IsEmpty + -> + Some(true, tryGetFirstAttributeLine attributes) + | SynMemberDefn.GetSetMember( + memberDefnForSet = Some(SynBinding( + attributes = attributes + xmlDoc = xmlDoc + headPat = SynPat.LongIdent(longDotId = longDotId)))) when + rangeContainsPos longDotId.Range pos && xmlDoc.IsEmpty + -> + Some(true, tryGetFirstAttributeLine attributes) + | SynMemberDefn.GetSetMember( + memberDefnForGet = Some(SynBinding( + attributes = attributes + xmlDoc = xmlDoc + headPat = SynPat.LongIdent(longDotId = longDotId)))) when + rangeContainsPos longDotId.Range pos && xmlDoc.IsEmpty + -> + Some(true, tryGetFirstAttributeLine attributes) + | _ -> None) + | _ -> None) + | _ -> None) + + findNested decls } + ) let isAstElemWithEmptyPreXmlDoc input pos = - (pos, input) - ||> ParsedInput.tryPickLast (fun _path node -> - let (|AnyGetSetMemberInRange|_|) = - List.tryPick (function - | SynMemberDefn.GetSetMember( - memberDefnForSet = Some(SynBinding(xmlDoc = xmlDoc; headPat = SynPat.LongIdent(longDotId = longDotId)))) - | SynMemberDefn.GetSetMember( - memberDefnForGet = Some(SynBinding(xmlDoc = xmlDoc; headPat = SynPat.LongIdent(longDotId = longDotId)))) when - rangeContainsPos longDotId.Range pos && xmlDoc.IsEmpty - -> - Some() - | _ -> None) - - match node with - | SyntaxNode.SynBinding(SynBinding( - valData = SynValData(Some { MemberKind = SynMemberKind.PropertyGet }, _, _))) - | SyntaxNode.SynBinding(SynBinding( - valData = SynValData(Some { MemberKind = SynMemberKind.PropertySet }, _, _))) - | SyntaxNode.SynBinding(SynBinding( - valData = SynValData(Some { MemberKind = SynMemberKind.PropertyGetSet }, _, _))) -> None - - | SyntaxNode.SynBinding(SynBinding(xmlDoc = xmlDoc) as s) when - rangeContainsPos s.RangeOfBindingWithoutRhs pos && xmlDoc.IsEmpty - -> - Some false - - | SyntaxNode.SynTypeDefn(SynTypeDefn(typeRepr = SynTypeDefnRepr.ObjectModel(members = AnyGetSetMemberInRange))) -> - Some false - - | SyntaxNode.SynTypeDefn(SynTypeDefn(typeInfo = SynComponentInfo(longId = longId; xmlDoc = xmlDoc))) when - longIdentContainsPos longId pos && xmlDoc.IsEmpty - -> - Some false - - | SyntaxNode.SynTypeDefn(SynTypeDefn( - typeRepr = SynTypeDefnRepr.Simple(SynTypeDefnSimpleRepr.Record(recordFields = fields), _))) -> - let isInLine c = - match c with - | SynField(xmlDoc = xmlDoc; idOpt = Some ident) when rangeContainsPos ident.idRange pos && xmlDoc.IsEmpty -> - Some false - | _ -> None - - fields |> List.tryPick isInLine - - | SyntaxNode.SynTypeDefn(SynTypeDefn( - typeRepr = SynTypeDefnRepr.Simple(SynTypeDefnSimpleRepr.Union(unionCases = cases), _))) -> - let isInLine c = - match c with - | SynUnionCase(xmlDoc = xmlDoc; ident = SynIdent(ident = ident)) when - rangeContainsPos ident.idRange pos && xmlDoc.IsEmpty - -> - Some false - | _ -> None - - cases |> List.tryPick isInLine - - | SyntaxNode.SynTypeDefn(SynTypeDefn( - typeRepr = SynTypeDefnRepr.Simple(SynTypeDefnSimpleRepr.Enum(cases = cases), _))) -> - let isInLine c = - match c with - | SynEnumCase(xmlDoc = xmlDoc; ident = SynIdent(ident = ident)) when - rangeContainsPos ident.idRange pos && xmlDoc.IsEmpty - -> - Some false - | _ -> None - - cases |> List.tryPick isInLine - - | SyntaxNode.SynModuleOrNamespace(SynModuleOrNamespace(longId = longId; xmlDoc = xmlDoc)) when - longIdentContainsPos longId pos && xmlDoc.IsEmpty - -> - Some false - - | SyntaxNode.SynModule(SynModuleDecl.NestedModule( - moduleInfo = SynComponentInfo(longId = longId; xmlDoc = xmlDoc))) when - longIdentContainsPos longId pos && xmlDoc.IsEmpty - -> - Some false - - | SyntaxNode.SynMemberDefn(SynMemberDefn.AutoProperty(ident = ident; xmlDoc = xmlDoc)) when - rangeContainsPos ident.idRange pos && xmlDoc.IsEmpty - -> - Some true - - | SyntaxNode.SynMemberDefn(SynMemberDefn.GetSetMember( - memberDefnForGet = Some(SynBinding(xmlDoc = xmlDoc; headPat = SynPat.LongIdent(longDotId = longDotId))))) - | SyntaxNode.SynMemberDefn(SynMemberDefn.GetSetMember( - memberDefnForSet = Some(SynBinding(xmlDoc = xmlDoc; headPat = SynPat.LongIdent(longDotId = longDotId))))) when - rangeContainsPos longDotId.Range pos && xmlDoc.IsEmpty - -> - Some false - - | _ -> None) + match isLowerAstElemWithEmptyPreXmlDoc input pos with + | Some(isAutoProperty, firstAttrLine) -> Some(isAutoProperty, firstAttrLine) + | _ -> isModuleOrNamespaceOrAutoPropertyWithEmptyPreXmlDoc input pos let trimmed = lineStr.TrimStart(' ') let indentLength = lineStr.Length - trimmed.Length @@ -1369,7 +1423,7 @@ type Commands() = match isAstElemWithEmptyPreXmlDoc tyRes.GetAST triggerPosition with | None -> return None - | Some(isAutoProperty) -> + | Some(isAutoProperty, firstAttrLine) -> let signatureData = Commands.SignatureData tyRes triggerPosition lineStr |> Result.ofCoreResponse @@ -1409,7 +1463,8 @@ type Commands() = |> fun s -> s + Environment.NewLine // need a newline at the very end // always insert at the start of the line, because we've prepended the indent to the start of the summary section - let insertPosition = Position.mkPos triggerPosition.Line 0 + let insertPosLine = firstAttrLine |> Option.defaultValue triggerPosition.Line + let insertPosition = Position.mkPos insertPosLine 0 return Some diff --git a/src/FsAutoComplete.Core/CompilerServiceInterface.fs b/src/FsAutoComplete.Core/CompilerServiceInterface.fs index 327f4c4af..2a6a30cd4 100644 --- a/src/FsAutoComplete.Core/CompilerServiceInterface.fs +++ b/src/FsAutoComplete.Core/CompilerServiceInterface.fs @@ -440,11 +440,8 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe /// Note: all files except the one being checked are read from the FileSystem API /// Result of ParseAndCheckResults member _.ParseAndCheckFileInProject - ( - filePath: string, - snapshot: FSharpProjectSnapshot, - ?shouldCache: bool - ) = + (filePath: string, snapshot: FSharpProjectSnapshot, ?shouldCache: bool) + = asyncResult { let shouldCache = defaultArg shouldCache false let opName = sprintf "ParseAndCheckFileInProject - %A" filePath @@ -495,13 +492,8 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe } member __.ParseAndCheckFileInProject - ( - filePath: string, - version, - source: ISourceText, - options, - ?shouldCache: bool - ) = + (filePath: string, version, source: ISourceText, options, ?shouldCache: bool) + = asyncResult { let shouldCache = defaultArg shouldCache false let opName = sprintf "ParseAndCheckFileInProject - %A" filePath @@ -620,11 +612,8 @@ type FSharpCompilerServiceChecker(hasAnalyzers, typecheckCacheSize, parallelRefe | CompilerProjectOption.TransparentCompiler snapshot -> checker.ParseAndCheckProject(snapshot) member x.GetUsesOfSymbol - ( - file: string, - snapshots: (string * CompilerProjectOption) seq, - symbol: FSharpSymbol - ) = + (file: string, snapshots: (string * CompilerProjectOption) seq, symbol: FSharpSymbol) + = async { checkerLogger.info ( Log.setMessage "GetUsesOfSymbol - {file}" diff --git a/src/FsAutoComplete.Core/FileSystem.fs b/src/FsAutoComplete.Core/FileSystem.fs index e02d61cf8..43744a574 100644 --- a/src/FsAutoComplete.Core/FileSystem.fs +++ b/src/FsAutoComplete.Core/FileSystem.fs @@ -623,11 +623,8 @@ module Tokenizer = /// /// based on: `dotnet/fsharp` `Tokenizer.fixupSpan` let private tryFixupRangeBySplittingAtDot - ( - range: Range, - text: IFSACSourceText, - includeBackticks: bool - ) : Range voption = + (range: Range, text: IFSACSourceText, includeBackticks: bool) + : Range voption = match text[range] with | Error _ -> ValueNone | Ok rangeText when rangeText.EndsWith("``", StringComparison.Ordinal) -> @@ -689,12 +686,8 @@ module Tokenizer = /// /// returns `None` iff `range` isn't inside `text` -> `range` & `text` for different states let tryFixupRange - ( - symbolNameCore: string, - range: Range, - text: IFSACSourceText, - includeBackticks: bool - ) : Range voption = + (symbolNameCore: string, range: Range, text: IFSACSourceText, includeBackticks: bool) + : Range voption = // first: try match symbolNameCore in last line // usually identifier cannot contain linebreak -> is in last line of range // Exception: Active Pattern can span multiple lines: `(|Even|Odd|)` -> `(|Even|\n Odd|)` is valid too diff --git a/src/FsAutoComplete.Core/InlayHints.fs b/src/FsAutoComplete.Core/InlayHints.fs index 10750bf22..2182b6b3f 100644 --- a/src/FsAutoComplete.Core/InlayHints.fs +++ b/src/FsAutoComplete.Core/InlayHints.fs @@ -815,12 +815,8 @@ type HintConfig = ShowParameterHints: bool } let provideHints - ( - text: IFSACSourceText, - parseAndCheck: ParseAndCheckResults, - range: Range, - hintConfig - ) : Async = + (text: IFSACSourceText, parseAndCheck: ParseAndCheckResults, range: Range, hintConfig) + : Async = asyncResult { let! cancellationToken = Async.CancellationToken diff --git a/src/FsAutoComplete.Core/Lexer.fs b/src/FsAutoComplete.Core/Lexer.fs index 3168352b7..128564a9f 100644 --- a/src/FsAutoComplete.Core/Lexer.fs +++ b/src/FsAutoComplete.Core/Lexer.fs @@ -89,10 +89,8 @@ module Lexer = let inline private isPunctuation t = t.ColorClass = FSharpTokenColorKind.Punctuation let inline private (|GenericTypeParameterPrefix|StaticallyResolvedTypeParameterPrefix|ActivePattern|Other|) - ( - (token: FSharpTokenInfo), - (lineStr: string) - ) = + ((token: FSharpTokenInfo), (lineStr: string)) + = if token.Tag = FSharpTokenTag.QUOTE then GenericTypeParameterPrefix elif token.Tag = FSharpTokenTag.INFIX_AT_HAT_OP then diff --git a/src/FsAutoComplete.Core/ParseAndCheckResults.fsi b/src/FsAutoComplete.Core/ParseAndCheckResults.fsi index 524615055..b254642b3 100644 --- a/src/FsAutoComplete.Core/ParseAndCheckResults.fsi +++ b/src/FsAutoComplete.Core/ParseAndCheckResults.fsi @@ -54,11 +54,10 @@ type ParseAndCheckResults = member TryGetFormattedDocumentation: pos: Position -> lineStr: LineStr -> - Result<(ToolTipText option * - (string * string) option * - (string * DocumentationFormatter.EntityInfo) * - string * - string), string> + Result< + (ToolTipText option * (string * string) option * (string * DocumentationFormatter.EntityInfo) * string * string), + string + > member TryGetFormattedDocumentationForSymbol: xmlSig: string -> diff --git a/src/FsAutoComplete.Core/SignatureHelp.fs b/src/FsAutoComplete.Core/SignatureHelp.fs index e3f059674..e7067263e 100644 --- a/src/FsAutoComplete.Core/SignatureHelp.fs +++ b/src/FsAutoComplete.Core/SignatureHelp.fs @@ -27,12 +27,8 @@ type SignatureHelpInfo = } let private getSignatureHelpForFunctionApplication - ( - tyRes: ParseAndCheckResults, - caretPos: Position, - endOfPreviousIdentPos: Position, - lines: IFSACSourceText - ) : Async = + (tyRes: ParseAndCheckResults, caretPos: Position, endOfPreviousIdentPos: Position, lines: IFSACSourceText) + : Async = asyncOption { let! lineStr = lines.GetLine endOfPreviousIdentPos @@ -124,12 +120,8 @@ let private getSignatureHelpForFunctionApplication } let private getSignatureHelpForMethod - ( - tyRes: ParseAndCheckResults, - caretPos: Position, - lines: IFSACSourceText, - triggerChar - ) = + (tyRes: ParseAndCheckResults, caretPos: Position, lines: IFSACSourceText, triggerChar) + = asyncOption { let! paramLocations = tyRes.GetParseResults.FindParameterLocations caretPos let names = paramLocations.LongId @@ -213,13 +205,8 @@ let private getSignatureHelpForMethod } let getSignatureHelpFor - ( - tyRes: ParseAndCheckResults, - pos: Position, - lines: IFSACSourceText, - triggerChar, - possibleSessionKind - ) = + (tyRes: ParseAndCheckResults, pos: Position, lines: IFSACSourceText, triggerChar, possibleSessionKind) + = asyncResult { let previousNonWhitespaceChar = let rec loop ch pos = diff --git a/src/FsAutoComplete.Core/TipFormatter.fsi b/src/FsAutoComplete.Core/TipFormatter.fsi index 1fe98eb3f..caa81f531 100644 --- a/src/FsAutoComplete.Core/TipFormatter.fsi +++ b/src/FsAutoComplete.Core/TipFormatter.fsi @@ -108,8 +108,10 @@ val prepareFooterLines: footerText: string -> string array val tryFormatTipEnhanced: toolTipText: ToolTipText -> formatCommentStyle: FormatCommentStyle -> - TipFormatterResult<{| DocComment: string - HasTruncatedExamples: bool |}> + TipFormatterResult< + {| DocComment: string + HasTruncatedExamples: bool |} + > /// /// Generate the 'Show documentation' link for the tooltip. diff --git a/src/FsAutoComplete.Logging/FsLibLog.fs b/src/FsAutoComplete.Logging/FsLibLog.fs index e45504d8c..7ff48103a 100644 --- a/src/FsAutoComplete.Logging/FsLibLog.fs +++ b/src/FsAutoComplete.Logging/FsLibLog.fs @@ -48,45 +48,9 @@ module Types = abstract member Log: Logger abstract member MappedContext: MappedContext -#if FABLE_COMPILER - // Fable doesn't support System.Collections.Generic.Stack, so this implementation (from FCS) - // is used instead. - type Stack<'a>() = - let mutable contents = Array.zeroCreate<'a> (2) - let mutable count = 0 - - member buf.Ensure newSize = - let oldSize = contents.Length - - if newSize > oldSize then - let old = contents - contents <- Array.zeroCreate (max newSize (oldSize * 2)) - Array.blit old 0 contents 0 count - - member buf.Count = count - - member buf.Pop() = - let item = contents.[count - 1] - count <- count - 1 - item - - member buf.Peep() = contents.[count - 1] - - member buf.Top(n) = [ for x in contents.[max 0 (count - n) .. count - 1] -> x ] |> List.rev - - member buf.Push(x) = - buf.Ensure(count + 1) - contents.[count] <- x - count <- count + 1 - - member buf.IsEmpty = (count = 0) -#endif - [] module Inner = -#if !FABLE_COMPILER open System.Collections.Generic -#endif /// /// DisposableStack on Dispose will call dispose on items appended to its stack in Last In First Out. @@ -310,8 +274,6 @@ module Types = /// The amended log. let setLogLevel (logLevel: LogLevel) (log: Log) = { log with LogLevel = logLevel } -#if !FABLE_COMPILER - let private formatterRegex = Regex(@"(?\d+)(?:(?[^}]+))?}(?!})", RegexOptions.Compiled) @@ -377,7 +339,6 @@ module Types = /// The log to amend. /// The amended log. let setMessageI (message: FormattableString) (log: Log) = setMessageInterpolated message log -#endif /// Provides operators to make writing logs more streamlined. module Operators = @@ -431,8 +392,6 @@ module Operators = /// The amended log with the parameter added. let (>>!!) log e = log >> Log.addException e - -#if !FABLE_COMPILER module Providers = module SerilogProvider = open System @@ -904,27 +863,20 @@ module Providers = let create () = MicrosoftProvider() :> ILogProvider -#endif module LogProvider = open System open Types -#if !FABLE_COMPILER open Providers -#endif open System.Diagnostics open Microsoft.FSharp.Quotations.Patterns let mutable private currentLogProvider = None let private knownProviders = - [ -#if !FABLE_COMPILER - (SerilogProvider.isAvailable, SerilogProvider.create) - (MicrosoftExtensionsLoggingProvider.isAvailable, MicrosoftExtensionsLoggingProvider.create) -#endif - ] + [ (SerilogProvider.isAvailable, SerilogProvider.create) + (MicrosoftExtensionsLoggingProvider.isAvailable, MicrosoftExtensionsLoggingProvider.create) ] /// Greedy search for first available LogProvider. Order of known providers matters. let private resolvedLogger = @@ -1024,7 +976,6 @@ module LogProvider = /// let inline getLoggerFor<'a> () = getLoggerByType (typeof<'a>) -#if !FABLE_COMPILER let rec private getModuleType = function | PropertyGet(_, propertyInfo, _) -> propertyInfo.DeclaringType @@ -1065,5 +1016,3 @@ type LogProvider = mi.DeclaringType.FullName.Split('+') |> Seq.tryHead |> Option.defaultValue "" sprintf "%s.%s" location memberName.Value |> LogProvider.getLoggerByName - -#endif diff --git a/src/FsAutoComplete.Logging/FsOpenTelemetry.fs b/src/FsAutoComplete.Logging/FsOpenTelemetry.fs index ad467cc68..356f94b41 100644 --- a/src/FsAutoComplete.Logging/FsOpenTelemetry.fs +++ b/src/FsAutoComplete.Logging/FsOpenTelemetry.fs @@ -544,26 +544,20 @@ type ActivityExtensions = [] static member inline SetNetworkNetTransport - ( - span: Activity, - value: string - ) = + (span: Activity, value: string) + = span.SetTagSafe(SemanticConventions.General.Network.net_transport, UMX.untag value) [] static member inline SetNetworkNetHostConnectionType - ( - span: Activity, - value: string - ) = + (span: Activity, value: string) + = span.SetTagSafe(SemanticConventions.General.Network.net_host_connection_type, UMX.untag value) [] static member inline SetNetworkNetHostConnectionSubType - ( - span: Activity, - value: string - ) = + (span: Activity, value: string) + = span.SetTagSafe(SemanticConventions.General.Network.net_host_connection_subtype, UMX.untag value) @@ -575,13 +569,8 @@ type ActivityExtensions = /// 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 RecordError - ( - span: Activity, - errorMessage: string, - errorType: string, - ?stacktrace: string, - ?escaped: bool - ) = + (span: Activity, errorMessage: string, errorType: string, ?stacktrace: string, ?escaped: bool) + = if Funcs.isNotNull span then let escaped = defaultArg escaped false diff --git a/src/FsAutoComplete/CodeFixes.fs b/src/FsAutoComplete/CodeFixes.fs index afad75852..b6469a651 100644 --- a/src/FsAutoComplete/CodeFixes.fs +++ b/src/FsAutoComplete/CodeFixes.fs @@ -37,6 +37,7 @@ module Types = type GetProjectOptionsForFile = string -> Async> [] + [] type FixKind = | Fix | Refactor diff --git a/src/FsAutoComplete/CodeFixes.fsi b/src/FsAutoComplete/CodeFixes.fsi index 3ab46279a..a3d58f49d 100644 --- a/src/FsAutoComplete/CodeFixes.fsi +++ b/src/FsAutoComplete/CodeFixes.fsi @@ -30,6 +30,7 @@ module Types = type GetProjectOptionsForFile = string -> Async> [] + [] type FixKind = | Fix | Refactor diff --git a/src/FsAutoComplete/CodeFixes/AddExplicitTypeAnnotation.fs b/src/FsAutoComplete/CodeFixes/AddExplicitTypeAnnotation.fs index 70e8ea2ef..a64a9b240 100644 --- a/src/FsAutoComplete/CodeFixes/AddExplicitTypeAnnotation.fs +++ b/src/FsAutoComplete/CodeFixes/AddExplicitTypeAnnotation.fs @@ -26,6 +26,7 @@ let rec nonTypedParameterName p = | _ -> None /// Captures a SynBinding that either has no return type or has parameters that are not typed. +[] let (|FunctionBindingWithMissingTypes|_|) = function | SynBinding( @@ -33,7 +34,7 @@ let (|FunctionBindingWithMissingTypes|_|) = returnInfo = None trivia = { LeadingKeyword = lk }) -> let bindingStartRange = unionRanges lk.Range lid.Range - Some(bindingStartRange, Some headPat.Range, parameters.Length, List.choose nonTypedParameterName parameters) + ValueSome(bindingStartRange, Some headPat.Range, parameters.Length, List.choose nonTypedParameterName parameters) | SynBinding( headPat = SynPat.LongIdent(longDotId = lid; argPats = SynArgPats.Pats parameters) returnInfo = Some _ @@ -42,10 +43,10 @@ let (|FunctionBindingWithMissingTypes|_|) = let nonTypedParameters = List.choose nonTypedParameterName parameters if List.isEmpty nonTypedParameters then - None + ValueNone else - Some(bindingStartRange, None, parameters.Length, nonTypedParameters) - | _ -> None + ValueSome(bindingStartRange, None, parameters.Length, nonTypedParameters) + | _ -> ValueNone /// /// Try and find a SynBinding function where either the return type or any parameter is missing a type definition. diff --git a/src/FsAutoComplete/CodeFixes/AddPrivateAccessModifier.fs b/src/FsAutoComplete/CodeFixes/AddPrivateAccessModifier.fs index 044b535dd..b4eb1530a 100644 --- a/src/FsAutoComplete/CodeFixes/AddPrivateAccessModifier.fs +++ b/src/FsAutoComplete/CodeFixes/AddPrivateAccessModifier.fs @@ -19,8 +19,14 @@ type SymbolUseWorkspace = -> LineStr -> IFSACSourceText -> ParseAndCheckResults - -> Async, FSharp.Compiler.Text.range array>, string>> - + -> Async< + Result< + System.Collections.Generic.IDictionary, FSharp.Compiler.Text.range array>, + string + > + > + +[] type private Placement = | Before | After diff --git a/src/FsAutoComplete/CodeFixes/AddPrivateAccessModifier.fsi b/src/FsAutoComplete/CodeFixes/AddPrivateAccessModifier.fsi index 932009bbf..38c91f6ad 100644 --- a/src/FsAutoComplete/CodeFixes/AddPrivateAccessModifier.fsi +++ b/src/FsAutoComplete/CodeFixes/AddPrivateAccessModifier.fsi @@ -19,7 +19,12 @@ type SymbolUseWorkspace = -> LineStr -> IFSACSourceText -> ParseAndCheckResults - -> Async, FSharp.Compiler.Text.range array>, string>> + -> Async< + Result< + System.Collections.Generic.IDictionary, FSharp.Compiler.Text.range array>, + string + > + > val fix: getParseResultsForFile: GetParseResultsForFile -> diff --git a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs index ec671a3ff..259effe81 100644 --- a/src/FsAutoComplete/CodeFixes/AdjustConstant.fs +++ b/src/FsAutoComplete/CodeFixes/AdjustConstant.fs @@ -267,6 +267,7 @@ module private Char = let inline isSingleQuote c = c = '\'' [] +[] type CharFormat = /// `ç` | Char @@ -338,6 +339,7 @@ module private CharConstant = ValueRange = valueRange SuffixRange = suffixRange } +[] type private Sign = | Negative | Positive @@ -357,6 +359,7 @@ module private Sign = Positive, range.EmptyAtStart, range [] +[] type Base = /// No prefix | Decimal @@ -430,10 +433,11 @@ module private IntConstant = SuffixRange = suffixRange } [] +[] type private FloatValue = - | Float of float - | Float32 of float32 - | Decimal of decimal + | Float of f: float + | Float32 of f32: float32 + | Decimal of d: decimal static member from(f: float) = FloatValue.Float f static member from(f: float32) = FloatValue.Float32 f @@ -583,6 +587,7 @@ module private DigitGroup = else [] + [] type Direction = /// thousands -> left of `.` | RightToLeft diff --git a/src/FsAutoComplete/CodeFixes/AdjustConstant.fsi b/src/FsAutoComplete/CodeFixes/AdjustConstant.fsi index a3972ed2a..9bedc84d6 100644 --- a/src/FsAutoComplete/CodeFixes/AdjustConstant.fsi +++ b/src/FsAutoComplete/CodeFixes/AdjustConstant.fsi @@ -4,6 +4,7 @@ open FsAutoComplete.CodeFix.Types open Ionide.LanguageServerProtocol.Types [] +[] type CharFormat = /// `ç` | Char @@ -17,6 +18,7 @@ type CharFormat = | Utf32Hexadecimal [] +[] type Base = /// No prefix | Decimal diff --git a/src/FsAutoComplete/CodeFixes/ConvertPositionalDUToNamed.fs b/src/FsAutoComplete/CodeFixes/ConvertPositionalDUToNamed.fs index f05c08598..26441afb3 100644 --- a/src/FsAutoComplete/CodeFixes/ConvertPositionalDUToNamed.fs +++ b/src/FsAutoComplete/CodeFixes/ConvertPositionalDUToNamed.fs @@ -117,8 +117,7 @@ let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = let commasBetweenFields = toPosSeq (parenRange, sourceText) - |> Seq.filter notInsidePatterns - |> Seq.filter (fun pos -> sourceText.GetCharUnsafe pos = ',') + |> Seq.filter (fun pos -> notInsidePatterns pos && sourceText.GetCharUnsafe pos = ',') let removeCommaEdits = commasBetweenFields diff --git a/src/FsAutoComplete/CodeFixes/ExprTypeMismatch.fs b/src/FsAutoComplete/CodeFixes/ExprTypeMismatch.fs new file mode 100644 index 000000000..7832e343b --- /dev/null +++ b/src/FsAutoComplete/CodeFixes/ExprTypeMismatch.fs @@ -0,0 +1,117 @@ +module FsAutoComplete.CodeFix.ExprTypeMismatch + +#nowarn "57" + +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 + +let findReturnType (cursor: pos) (tree: ParsedInput) = + let visitor = + { new SyntaxVisitorBase() with + member _.VisitBinding(path, defaultTraverse, synBinding) = + match synBinding with + | SynBinding(returnInfo = Some(SynBindingReturnInfo(typeName = t)); expr = bodyExpr) when + Range.rangeContainsPos bodyExpr.Range cursor + -> + Some t.Range + | _ -> None } + + SyntaxTraversal.Traverse(cursor, tree, visitor) + +let needParenthesisWhenWrappedInSome (diagnosticRange: range) (tree: ParsedInput) = + let visitor = + { new SyntaxVisitorBase() with + member _.VisitExpr(path, traverseSynExpr, defaultTraverse, synExpr) = + if not (Range.equals synExpr.Range diagnosticRange) then + defaultTraverse synExpr + else + match synExpr with + | SynExpr.Const _ + | SynExpr.Ident _ -> Some false + | e -> defaultTraverse e } + + SyntaxTraversal.Traverse(diagnosticRange.Start, tree, visitor) + |> Option.defaultValue true + +let title = "ExprTypeMismatch Codefix" + +let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = + Run.ifDiagnosticByCode (set [ "1" ]) (fun diagnostic (codeActionParams: CodeActionParams) -> + asyncResult { + let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath + let fcsPos = protocolPosToPos diagnostic.Range.Start + + let! (parseAndCheckResults: ParseAndCheckResults, _line: string, sourceText: IFSACSourceText) = + getParseResultsForFile fileName fcsPos + + let diagnosticWithExtendedData = + parseAndCheckResults.GetCheckResults.Diagnostics + |> Array.tryPick (fun d -> + match d.ExtendedData with + | Some(:? TypeMismatchDiagnosticExtendedData as data) -> Some(d, data) + | _ -> None) + + match diagnosticWithExtendedData with + | None -> return [] + | Some(diagnostic, extendedData) -> + let updateReturnType = + findReturnType fcsPos parseAndCheckResults.GetParseResults.ParseTree + |> Option.map (fun mReturnType -> + let currentType = sourceText.GetSubTextFromRange mReturnType + let actualType = extendedData.ActualType.Format(extendedData.DisplayContext) + + { SourceDiagnostic = None + Title = $"Update %s{currentType} to %s{actualType}" + File = codeActionParams.TextDocument + Edits = + [| { Range = fcsRangeToLsp mReturnType + NewText = actualType } |] + Kind = FixKind.Fix }) + |> Option.toList + + let optionFixes = + if diagnostic.Range.StartLine <> diagnostic.Range.EndLine then + [] + elif + extendedData.ExpectedType.BasicQualifiedName = "Microsoft.FSharp.Core.option`1" + || extendedData.ExpectedType.BasicQualifiedName = "Microsoft.FSharp.Core.voption`1" + then + let currentExpr = sourceText.GetSubTextFromRange diagnostic.Range + + let isValueOption = + extendedData.ExpectedType.BasicQualifiedName = "Microsoft.FSharp.Core.voption`1" + + let wrapIn = if isValueOption then "ValueSome" else "Some" + let replaceWithNone = if isValueOption then "ValueNone" else "None" + + let needsParenthesis = + needParenthesisWhenWrappedInSome diagnostic.Range parseAndCheckResults.GetParseResults.ParseTree + + let space, openP, closeP = + if not needsParenthesis then " ", "", "" else "", "(", ")" + + [ { SourceDiagnostic = None + Title = $"Wrap expression in %s{wrapIn}" + File = codeActionParams.TextDocument + Edits = + [| { Range = fcsRangeToLsp diagnostic.Range + NewText = $"%s{wrapIn}%s{space}%s{openP}%s{currentExpr}%s{closeP}" } |] + Kind = FixKind.Fix } + { SourceDiagnostic = None + Title = $"Replace expression with %s{replaceWithNone}" + File = codeActionParams.TextDocument + Edits = + [| { Range = fcsRangeToLsp diagnostic.Range + NewText = replaceWithNone } |] + Kind = FixKind.Fix } ] + else + [] + + return [ yield! updateReturnType; yield! optionFixes ] + }) diff --git a/src/FsAutoComplete/CodeFixes/ExprTypeMismatch.fsi b/src/FsAutoComplete/CodeFixes/ExprTypeMismatch.fsi new file mode 100644 index 000000000..201765b28 --- /dev/null +++ b/src/FsAutoComplete/CodeFixes/ExprTypeMismatch.fsi @@ -0,0 +1,6 @@ +module FsAutoComplete.CodeFix.ExprTypeMismatch + +open FsAutoComplete.CodeFix.Types + +val title: string +val fix: getParseResultsForFile: GetParseResultsForFile -> CodeFix diff --git a/src/FsAutoComplete/CodeFixes/ExternalSystemDiagnostics.fs b/src/FsAutoComplete/CodeFixes/ExternalSystemDiagnostics.fs index b38381978..df47b0f98 100644 --- a/src/FsAutoComplete/CodeFixes/ExternalSystemDiagnostics.fs +++ b/src/FsAutoComplete/CodeFixes/ExternalSystemDiagnostics.fs @@ -7,11 +7,12 @@ open Ionide.LanguageServerProtocol.Types open FsAutoComplete open Newtonsoft.Json.Linq -let private (|Payload|_|) (tok: JToken) : 't option = +[] +let private (|Payload|_|) (tok: JToken) : 't voption = try - Some(Ionide.LanguageServerProtocol.Server.deserialize tok: 't) + ValueSome(Ionide.LanguageServerProtocol.Server.deserialize tok: 't) with e -> - None + ValueNone let private mapExternalDiagnostic diagnosticType = Run.ifDiagnosticByType diagnosticType (fun diagnostic codeActionParams -> diff --git a/src/FsAutoComplete/CodeFixes/RemoveUnnecessaryParentheses.fs b/src/FsAutoComplete/CodeFixes/RemoveUnnecessaryParentheses.fs index 56f3e091a..fbc5f97ab 100644 --- a/src/FsAutoComplete/CodeFixes/RemoveUnnecessaryParentheses.fs +++ b/src/FsAutoComplete/CodeFixes/RemoveUnnecessaryParentheses.fs @@ -46,48 +46,19 @@ module private Patterns = | None -> ValueNone - /// Trim only spaces from the start if there is something else - /// before the open paren on the same line (or else we could move - /// the whole inner expression up a line); otherwise trim all whitespace - /// from start and end. - let (|Trim|) (range: FcsRange) (sourceText: IFSACSourceText) = - match sourceText.GetLine range.Start with - | Some line -> - if line.AsSpan(0, range.Start.Column).LastIndexOfAnyExcept(' ', '(') >= 0 then - fun (s: string) -> s.TrimEnd().TrimStart ' ' - else - fun (s: string) -> s.Trim() +[] +type private InnerOffsides = + /// We haven't found an inner construct yet. + | NoneYet - | None -> id - - /// Returns the offsides diff if the given span contains an expression - /// whose indentation would be made invalid if the open paren - /// were removed (because the offside line would be shifted). - [] - let (|OffsidesDiff|_|) (range: FcsRange) (sourceText: IFSACSourceText) = - let startLineNo = range.StartLine - let endLineNo = range.EndLine - - if startLineNo = endLineNo then - ValueNone - else - let rec loop innerOffsides (pos: FcsPos) (startCol: int) = - if pos.Line <= endLineNo then - match sourceText.GetLine pos with - | None -> ValueNone - | Some line -> - match line.AsSpan(startCol).IndexOfAnyExcept(' ', ')') with - | -1 -> loop innerOffsides (pos.IncLine()) 0 - | i -> loop (i + startCol) (pos.IncLine()) 0 - else - ValueSome(range.StartColumn - innerOffsides) + /// The start column of the first inner construct we find. + /// This may not be on the same line as the open paren. + | FirstLine of col: int - loop range.StartColumn range.Start (range.StartColumn + 1) - - let (|ShiftLeft|NoShift|ShiftRight|) n = - if n < 0 then ShiftLeft -n - elif n = 0 then NoShift - else ShiftRight n + /// The leftmost start column of an inner construct on a line + /// following the first inner construct we found. + /// We keep the first column of the first inner construct for comparison at the end. + | FollowingLine of firstLine: int * followingLine: int /// A codefix that removes unnecessary parentheses from the source. let fix (getFileLines: GetFileLines) : CodeFix = @@ -104,17 +75,61 @@ let fix (getFileLines: GetFileLines) : CodeFix = match firstChar, lastChar with | '(', ')' -> + /// Trim only spaces from the start if there is something else + /// before the open paren on the same line (or else we could move + /// the whole inner expression up a line); otherwise trim all whitespace + /// from start and end. + let (|Trim|) (sourceText: IFSACSourceText) = + match sourceText.GetLine range.Start with + | Some line -> + if line.AsSpan(0, range.Start.Column).LastIndexOfAnyExcept(' ', '(') >= 0 then + fun (s: string) -> s.TrimEnd().TrimStart ' ' + else + fun (s: string) -> s.Trim() + + | None -> id + + let (|ShiftLeft|NoShift|ShiftRight|) (sourceText: IFSACSourceText) = + let startLineNo = range.StartLine + let endLineNo = range.EndLine + + if startLineNo = endLineNo then + NoShift + else + let outerOffsides = range.StartColumn + + let rec loop innerOffsides lineNo (startCol: int) = + if lineNo <= endLineNo then + let line = sourceText.Lines[lineNo].ToString() + + match line.AsSpan(startCol).IndexOfAnyExcept(' ', ')') with + | -1 -> loop innerOffsides (lineNo + 1) 0 + | i -> + match innerOffsides with + | NoneYet -> loop (FirstLine(i + startCol)) (lineNo + 1) 0 + | FirstLine innerOffsides -> loop (FollowingLine(innerOffsides, i + startCol)) (lineNo + 1) 0 + | FollowingLine(firstLine, innerOffsides) -> + loop (FollowingLine(firstLine, min innerOffsides (i + startCol))) (lineNo + 1) 0 + else + innerOffsides + + match loop NoneYet startLineNo (range.StartColumn + 1) with + | NoneYet -> NoShift + | FirstLine innerOffsides when innerOffsides < outerOffsides -> ShiftRight(outerOffsides - innerOffsides) + | FirstLine innerOffsides -> ShiftLeft(innerOffsides - outerOffsides) + | FollowingLine(firstLine, followingLine) -> + match firstLine - outerOffsides with + | 0 -> NoShift + | 1 when firstLine < followingLine -> NoShift + | primaryOffset when primaryOffset < 0 -> ShiftRight -primaryOffset + | primaryOffset -> ShiftLeft primaryOffset + let adjusted = match sourceText with | TrailingOpen range -> txt[1 .. txt.Length - 2].TrimEnd() - - | Trim range trim & OffsidesDiff range spaces -> - match spaces with - | NoShift -> trim txt[1 .. txt.Length - 2] - | ShiftLeft spaces -> trim (txt[1 .. txt.Length - 2].Replace("\n" + String(' ', spaces), "\n")) - | ShiftRight spaces -> trim (txt[1 .. txt.Length - 2].Replace("\n", "\n" + String(' ', spaces))) - - | _ -> txt[1 .. txt.Length - 2].Trim() + | Trim trim & NoShift -> trim txt[1 .. txt.Length - 2] + | Trim trim & ShiftLeft spaces -> trim (txt[1 .. txt.Length - 2].Replace("\n" + String(' ', spaces), "\n")) + | Trim trim & ShiftRight spaces -> trim (txt[1 .. txt.Length - 2].Replace("\n", "\n" + String(' ', spaces))) let newText = let (|ShouldPutSpaceBefore|_|) (s: string) = diff --git a/src/FsAutoComplete/CommandResponse.fs b/src/FsAutoComplete/CommandResponse.fs index 7d98794e5..f524b37fe 100644 --- a/src/FsAutoComplete/CommandResponse.fs +++ b/src/FsAutoComplete/CommandResponse.fs @@ -168,7 +168,7 @@ module CommandResponse = AdditionalInfo: Map } - and ProjectOutputType = + and [] ProjectOutputType = | Library | Exe | Custom of string diff --git a/src/FsAutoComplete/CommandResponse.fsi b/src/FsAutoComplete/CommandResponse.fsi index b28384d18..b68c16035 100644 --- a/src/FsAutoComplete/CommandResponse.fsi +++ b/src/FsAutoComplete/CommandResponse.fsi @@ -79,7 +79,7 @@ module CommandResponse = Items: List AdditionalInfo: Map } - and ProjectOutputType = + and [] ProjectOutputType = | Library | Exe | Custom of string diff --git a/src/FsAutoComplete/LspHelpers.fs b/src/FsAutoComplete/LspHelpers.fs index 52b7f2e56..ece9a9ff8 100644 --- a/src/FsAutoComplete/LspHelpers.fs +++ b/src/FsAutoComplete/LspHelpers.fs @@ -98,6 +98,11 @@ module Conversions = let urlForCompilerCode (number: int) = $"https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/compiler-messages/fs%04d{number}" + [] + let unicodeParagraphCharacter: string = "\u001d" + + let private handleUnicodeParagraph (message: string) = message.Replace(unicodeParagraphCharacter, Environment.NewLine) + let fcsErrorToDiagnostic (error: FSharpDiagnostic) = { Range = { Start = @@ -108,7 +113,7 @@ module Conversions = Character = error.EndColumn } } Severity = fcsSeverityToDiagnostic error.Severity Source = Some "F# Compiler" - Message = error.Message + Message = handleUnicodeParagraph error.Message Code = Some(string error.ErrorNumber) RelatedInformation = Some [||] Tags = None diff --git a/src/FsAutoComplete/LspHelpers.fsi b/src/FsAutoComplete/LspHelpers.fsi index 782fc48ed..2296192dc 100644 --- a/src/FsAutoComplete/LspHelpers.fsi +++ b/src/FsAutoComplete/LspHelpers.fsi @@ -36,6 +36,9 @@ module Conversions = val fcsRangeToLspLocation: range: FcsRange -> Location val findDeclToLspLocation: decl: FsAutoComplete.FindDeclarationResult -> Location + [] + val unicodeParagraphCharacter: string = "\u001d" + type TextDocumentIdentifier with member GetFilePath: unit -> string diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index 207c64fd9..6dd3e482e 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -265,10 +265,8 @@ type AdaptiveFSharpLspServer member __.ScriptFileProjectOptions = state.ScriptFileProjectOptions.Publish member private x.logUnimplementedRequest<'t, 'u> - ( - argValue: 't, - [] caller: string - ) = + (argValue: 't, [] caller: string) + = logger.info ( Log.setMessage $"{caller} request: {{params}}" >> Log.addContextDestructured "params" argValue @@ -277,10 +275,8 @@ type AdaptiveFSharpLspServer Helpers.notImplemented<'u> member private x.logIgnoredNotification<'t> - ( - argValue: 't, - [] caller: string - ) = + (argValue: 't, [] caller: string) + = logger.info ( Log.setMessage $"{caller} request: {{params}}" >> Log.addContextDestructured "params" argValue @@ -1954,8 +1950,6 @@ type AdaptiveFSharpLspServer let! tyRes = state.GetOpenFileTypeCheckResults filePath |> AsyncResult.ofStringErr - let _fcsRange = protocolRangeToRange (UMX.untag filePath) p.Range - let! pipelineHints = Commands.inlineValues volatileFile.Source tyRes let hints = @@ -3034,8 +3028,6 @@ module AdaptiveFSharpLspServer = None | _ -> None - let _strategy = StreamJsonRpcTracingStrategy(Tracing.fsacActivitySource) - let (|Flatten|_|) (e: exn) = match e with | :? AggregateException as aex -> diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index 730488eb2..1adba959a 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -815,9 +815,6 @@ type AdaptiveState tryFindProp "MSBuildAllProjects" props |> Option.map (fun v -> v.Split(';', StringSplitOptions.RemoveEmptyEntries)) - - - let loadProjects (loader: IWorkspaceLoader) binlogConfig projects = logger.debug (Log.setMessageI $"Enter loading projects") @@ -1524,11 +1521,14 @@ type AdaptiveState } let autoCompleteItems - : cmap * - (Position -> option) * - FSharp.Compiler.Syntax.ParsedInput> = + : cmap< + DeclName, + DeclarationListItem * + Position * + string * + (Position -> option) * + FSharp.Compiler.Syntax.ParsedInput + > = cmap () let getAutoCompleteByDeclName name = autoCompleteItems |> AMap.tryFind name @@ -2155,7 +2155,8 @@ type AdaptiveState UpdateTypeAbbreviationInSignatureFile.fix tryGetParseAndCheckResultsForFile AddBindingToSignatureFile.fix forceGetFSharpProjectOptions tryGetParseAndCheckResultsForFile ReplaceLambdaWithDotLambda.fix getLanguageVersion tryGetParseAndCheckResultsForFile - IgnoreExpression.fix tryGetParseAndCheckResultsForFile |]) + IgnoreExpression.fix tryGetParseAndCheckResultsForFile + ExprTypeMismatch.fix tryGetParseAndCheckResultsForFile |]) let forgetDocument (uri: DocumentUri) = async { @@ -2480,15 +2481,8 @@ type AdaptiveState member x.GetAutoCompleteNamespacesByDeclName declName = getAutoCompleteNamespacesByDeclName declName |> AVal.force member x.SymbolUseWorkspace - ( - includeDeclarations, - includeBackticks, - errorOnFailureToFixRange, - pos, - lineStr, - text, - tyRes - ) = + (includeDeclarations, includeBackticks, errorOnFailureToFixRange, pos, lineStr, text, tyRes) + = symbolUseWorkspace includeDeclarations includeBackticks errorOnFailureToFixRange pos lineStr text tyRes member x.GetDeclarationLocation(symbolUse, text) = getDeclarationLocation (symbolUse, text) diff --git a/src/FsAutoComplete/LspServers/Common.fs b/src/FsAutoComplete/LspServers/Common.fs index 2676d3e39..731924011 100644 --- a/src/FsAutoComplete/LspServers/Common.fs +++ b/src/FsAutoComplete/LspServers/Common.fs @@ -29,8 +29,11 @@ module Result = let lineLookupErr (r: - Result<'T, {| FileName: string - Position: FcsPos |}>) + Result< + 'T, + {| FileName: string + Position: FcsPos |} + >) = r |> Result.mapError (ErrorMsgUtils.formatLineLookErr >> JsonRpc.Error.InternalErrorMessage) @@ -57,8 +60,10 @@ type DiagnosticCollection(sendDiagnostics: DocumentUri -> Diagnostic[] -> Async< Map.toArray diags |> Array.collect (snd >> snd) |> sendDiagnostics uri let agents = - System.Collections.Concurrent.ConcurrentDictionary * - CancellationTokenSource>() + System.Collections.Concurrent.ConcurrentDictionary< + DocumentUri, + MailboxProcessor * CancellationTokenSource + >() let rec restartAgent (fileUri: DocumentUri) = removeAgent fileUri diff --git a/src/FsAutoComplete/Parser.fs b/src/FsAutoComplete/Parser.fs index dd9332990..121cd2b6d 100644 --- a/src/FsAutoComplete/Parser.fs +++ b/src/FsAutoComplete/Parser.fs @@ -198,7 +198,7 @@ module Parser = let warnOnUnknownOptions = Invocation.InvocationMiddleware(fun ctx next -> if - ctx.ParseResult.UnmatchedTokens = null + isNull ctx.ParseResult.UnmatchedTokens || ctx.ParseResult.UnmatchedTokens.Count = 0 then next.Invoke(ctx) @@ -267,7 +267,7 @@ module Parser = let sourcesToExclude = Matching.WithProperty( Constants.SourceContextPropertyName, - fun s -> s <> null && Array.contains s logSourcesToExclude + fun s -> not (isNull s) && Array.contains s logSourcesToExclude ) let verbositySwitch = LoggingLevelSwitch(logLevel) diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/ExprTypeMismatchTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/ExprTypeMismatchTests.fs new file mode 100644 index 000000000..44a9042a2 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/ExprTypeMismatchTests.fs @@ -0,0 +1,81 @@ +module private FsAutoComplete.Tests.CodeFixTests.ExprTypeMismatchTests + +open Expecto +open Helpers +open Utils.ServerTests +open Utils.CursorbasedTests +open FsAutoComplete.CodeFix + +let tests state = + serverTestList (nameof ExprTypeMismatch) state defaultConfigDto None (fun server -> + [ testCaseAsync "Update return type" + <| CodeFix.check + server + "let a b : int = $0\"meh\"" + Diagnostics.acceptAll + (CodeFix.withTitle "Update int to string") + "let a b : string = \"meh\"" + + testCaseAsync "Wrap constant in Some" + <| CodeFix.check + server + "let a b : int option = 1$0" + Diagnostics.acceptAll + (CodeFix.withTitle "Wrap expression in Some") + "let a b : int option = Some 1" + + testCaseAsync "Wrap expr in Some" + <| CodeFix.check + server + "let a b : bool option = true$0 = false" + Diagnostics.acceptAll + (CodeFix.withTitle "Wrap expression in Some") + "let a b : bool option = Some(true = false)" + + testCaseAsync "Wrap single indent in Some" + <| CodeFix.check + server + "let a b : bool option = let x = true in $0x" + Diagnostics.acceptAll + (CodeFix.withTitle "Wrap expression in Some") + "let a b : bool option = let x = true in Some x" + + testCaseAsync "Replace expression with None" + <| CodeFix.check + server + "let a b : int option = 1$0" + Diagnostics.acceptAll + (CodeFix.withTitle "Replace expression with None") + "let a b : int option = None" + + testCaseAsync "Wrap constant in ValueSome" + <| CodeFix.check + server + "let a b : int voption = 1$0" + Diagnostics.acceptAll + (CodeFix.withTitle "Wrap expression in ValueSome") + "let a b : int voption = ValueSome 1" + + testCaseAsync "Wrap expr in ValueSome" + <| CodeFix.check + server + "let a b : bool voption = true$0 = false" + Diagnostics.acceptAll + (CodeFix.withTitle "Wrap expression in ValueSome") + "let a b : bool voption = ValueSome(true = false)" + + testCaseAsync "Wrap single indent in ValueSome" + <| CodeFix.check + server + "let a b : bool voption = let x = true in $0x" + Diagnostics.acceptAll + (CodeFix.withTitle "Wrap expression in ValueSome") + "let a b : bool voption = let x = true in ValueSome x" + + testCaseAsync "Replace expression with ValueNone" + <| CodeFix.check + server + "let a b : int voption = 1$0" + Diagnostics.acceptAll + (CodeFix.withTitle "Replace expression with ValueNone") + "let a b : int voption = ValueNone" ]) diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs index 6eedc1e2d..33b18e362 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs @@ -1556,6 +1556,24 @@ let private generateXmlDocumentationTests state = let f x y = x + y """ + testCaseAsync "documentation for function with attribute" + <| CodeFix.check + server + """ + [] + let $0f x y = x + y + """ + Diagnostics.acceptAll + selectCodeFix + """ + /// + /// + /// + /// + [] + let f x y = x + y + """ + testCaseAsync "documentation for use" <| CodeFix.check server @@ -1589,6 +1607,23 @@ let private generateXmlDocumentationTests state = type MyRecord = { Foo: int } """ + testCaseAsync "documentation for record type with multiple attribute lists" + <| CodeFix.check + server + """ + [] + [] + type MyRec$0ord = { Foo: int } + """ + Diagnostics.acceptAll + selectCodeFix + """ + /// + [] + [] + type MyRecord = { Foo: int } + """ + testCaseAsync "documentation for discriminated union type" <| CodeFix.check server @@ -1717,7 +1752,6 @@ let private generateXmlDocumentationTests state = type MyClass() = let mutable someField = "" /// - /// /// member _.Name with get () = "foo" @@ -1759,7 +1793,6 @@ let private generateXmlDocumentationTests state = type MyClass() = let mutable someField = "" /// - /// /// member _.Name with set (x: string) = someField <- x @@ -3382,6 +3415,28 @@ let private removeUnnecessaryParenthesesTests state = let x = 3 let y = 99 y - x + """ + + testCaseAsync "Handles sensitive multiline expr well" + <| CodeFix.check + server + """ + let longVarName1 = 1 + let longVarName2 = 2 + ( + longFunctionName + longVarName1 + longVarName2 + )$0 + """ + (Diagnostics.expectCode "FSAC0004") + selector + """ + let longVarName1 = 1 + let longVarName2 = 2 + longFunctionName + longVarName1 + longVarName2 """ ]) let tests textFactory state = @@ -3439,4 +3494,5 @@ let tests textFactory state = UpdateTypeAbbreviationInSignatureFileTests.tests state AddBindingToSignatureFileTests.tests state ReplaceLambdaWithDotLambdaTests.tests state - IgnoreExpressionTests.tests state ] + IgnoreExpressionTests.tests state + ExprTypeMismatchTests.tests state ] diff --git a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs index bcd0b6994..19ae26e60 100644 --- a/test/FsAutoComplete.Tests.Lsp/CoreTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CoreTests.fs @@ -468,3 +468,40 @@ let closeTests state = let! diags = doc |> Document.waitForLatestDiagnostics (TimeSpan.FromSeconds 5.0) Expect.isNonEmpty diags "There should be no publishDiagnostics without any diags after close" }) ]) + +let diagnosticsTest state = + let server = + async { + let path = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "DiagnosticFormatting") + let! (server, events) = serverInitialize path defaultConfigDto state + let path = Path.Combine(path, "Program.fs") + do! waitForWorkspaceFinishedParsing events + return (server, events, path) + } + |> Async.Cache + + testList + "Diagnostics formatting Tests" + [ testCaseAsync + "replacing unicode paragraph by newline" + (async { + let! (server, events, path) = server + + let tdop: DidOpenTextDocumentParams = { TextDocument = loadDocument path } + do! server.TextDocumentDidOpen tdop + + let! compilerResults = waitForCompilerDiagnosticsForFile "Program.fs" events |> Async.StartChild + + match! compilerResults with + | Ok() -> failtest "should get an F# compiler checking error" + | Core.Result.Error errors -> + Expect.exists + errors + (fun error -> error.Code = Some "39" || error.Code = Some "41") + "should have an error FS0039(identifier not defined) or FS0041(a unique overload for method 'TryParse' could not be determined based on type information prior to this program point)" + + Expect.all + errors + (fun error -> not <| error.Message.Contains(unicodeParagraphCharacter)) + "message should not contains unicode paragraph characters" + }) ] diff --git a/test/FsAutoComplete.Tests.Lsp/GoToTests.fs b/test/FsAutoComplete.Tests.Lsp/GoToTests.fs index b8eb3639c..06085ed4d 100644 --- a/test/FsAutoComplete.Tests.Lsp/GoToTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/GoToTests.fs @@ -13,12 +13,32 @@ open Utils.Utils open Utils.TextEdit open Helpers.Expecto.ShadowedTimeouts +let executeProcess (wd: string) (processName: string) (processArgs: string) = + let psi = new Diagnostics.ProcessStartInfo(processName, processArgs) + psi.UseShellExecute <- false + psi.RedirectStandardOutput <- true + psi.RedirectStandardError <- true + psi.CreateNoWindow <- true + psi.WorkingDirectory <- wd + let proc = Diagnostics.Process.Start(psi) + let output = new Text.StringBuilder() + let error = new Text.StringBuilder() + proc.OutputDataReceived.Add(fun args -> output.Append(args.Data) |> ignore) + proc.ErrorDataReceived.Add(fun args -> error.Append(args.Data) |> ignore) + proc.BeginErrorReadLine() + proc.BeginOutputReadLine() + proc.WaitForExit() + {| ExitCode = proc.ExitCode; StdOut = output.ToString(); StdErr = error.ToString() |} + ///GoTo tests let private gotoTest state = let server = async { let path = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "GoToTests") + let csharpPath = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "GoToCSharp") + let _buildInfo = executeProcess csharpPath "dotnet" "build" + let! (server, event) = serverInitialize path defaultConfigDto state do! waitForWorkspaceFinishedParsing event @@ -266,6 +286,25 @@ let private gotoTest state = (sprintf "File '%s' should exist locally after being downloaded" localPath) }) + ptestCaseAsync + "Go-to-definition from C# file" + (async { + let! server, _path, externalPath, _definitionPath = server + + let p: TextDocumentPositionParams = + { TextDocument = { Uri = Path.FilePathToUri externalPath } + Position = { Line = 26; Character = 23 } } + + 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) -> + //Do some assertions + () + }) + testCaseAsync "Go-to-type-definition" (async { diff --git a/test/FsAutoComplete.Tests.Lsp/Program.fs b/test/FsAutoComplete.Tests.Lsp/Program.fs index e32b16470..88f1b69ee 100644 --- a/test/FsAutoComplete.Tests.Lsp/Program.fs +++ b/test/FsAutoComplete.Tests.Lsp/Program.fs @@ -73,6 +73,7 @@ let compilers = ] let lspTests = + testSequenced <| testList "lsp" [ for (loaderName, workspaceLoaderFactory) in loaders do @@ -133,6 +134,7 @@ let lspTests = UnusedDeclarationsTests.tests createServer EmptyFileTests.tests createServer CallHierarchy.tests createServer + diagnosticsTest createServer ] ] ] /// Tests that do not require a LSP server diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/DiagnosticFormatting/DiagnosticFormatting.fsproj b/test/FsAutoComplete.Tests.Lsp/TestCases/DiagnosticFormatting/DiagnosticFormatting.fsproj new file mode 100644 index 000000000..dd5c71e58 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/DiagnosticFormatting/DiagnosticFormatting.fsproj @@ -0,0 +1,12 @@ + + + + Exe + net6.0 + + + + + + + diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/DiagnosticFormatting/Program.fs b/test/FsAutoComplete.Tests.Lsp/TestCases/DiagnosticFormatting/Program.fs new file mode 100644 index 000000000..5c56d738b --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/DiagnosticFormatting/Program.fs @@ -0,0 +1,9 @@ +// Learn more about F# at http://fsharp.org + +open System + +[] +let main argv = + let isOk, integer = System.Int32.TryParse nope + printfn "Hello World from F#!" + 0 // return an integer exit code diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/GoToCSharp/Class1.cs b/test/FsAutoComplete.Tests.Lsp/TestCases/GoToCSharp/Class1.cs new file mode 100644 index 000000000..d2ae8af79 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/GoToCSharp/Class1.cs @@ -0,0 +1,7 @@ +namespace GoToCSharp { + + public class Class1 + { + + } +} diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/GoToCSharp/GoToCSharp.csproj b/test/FsAutoComplete.Tests.Lsp/TestCases/GoToCSharp/GoToCSharp.csproj new file mode 100644 index 000000000..72764a664 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/GoToCSharp/GoToCSharp.csproj @@ -0,0 +1,7 @@ + + + + netstandard2.0 + + + diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/GoToTests/External.fs b/test/FsAutoComplete.Tests.Lsp/TestCases/GoToTests/External.fs index 8a97f0085..264c5e912 100644 --- a/test/FsAutoComplete.Tests.Lsp/TestCases/GoToTests/External.fs +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/GoToTests/External.fs @@ -22,4 +22,6 @@ let o v = Some v type B() = member val Value = Some "" with get,set let b = B() -b.Value |> ignore \ No newline at end of file +b.Value |> ignore + +let cc = GoToCSharp.Class1() \ No newline at end of file diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/GoToTests/GoToTests.fsproj b/test/FsAutoComplete.Tests.Lsp/TestCases/GoToTests/GoToTests.fsproj index 2a3943360..8cec338ad 100644 --- a/test/FsAutoComplete.Tests.Lsp/TestCases/GoToTests/GoToTests.fsproj +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/GoToTests/GoToTests.fsproj @@ -11,4 +11,7 @@ + + + diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/GoToTests/GoToTests.sln b/test/FsAutoComplete.Tests.Lsp/TestCases/GoToTests/GoToTests.sln new file mode 100644 index 000000000..648191fdc --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/GoToTests/GoToTests.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "GoToTests", "GoToTests.fsproj", "{6800DC59-9582-42D8-AFCC-CF37FBAB0B47}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GoToCSharp", "..\GoToCSharp\GoToCSharp.csproj", "{9983CEFF-1E39-4DCD-BCF3-025919E52871}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6800DC59-9582-42D8-AFCC-CF37FBAB0B47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6800DC59-9582-42D8-AFCC-CF37FBAB0B47}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6800DC59-9582-42D8-AFCC-CF37FBAB0B47}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6800DC59-9582-42D8-AFCC-CF37FBAB0B47}.Release|Any CPU.Build.0 = Release|Any CPU + {9983CEFF-1E39-4DCD-BCF3-025919E52871}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9983CEFF-1E39-4DCD-BCF3-025919E52871}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9983CEFF-1E39-4DCD-BCF3-025919E52871}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9983CEFF-1E39-4DCD-BCF3-025919E52871}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal