diff --git a/VSFSharpExtension.sln b/VSFSharpExtension.sln new file mode 100644 index 00000000000..a4e19ff2fae --- /dev/null +++ b/VSFSharpExtension.sln @@ -0,0 +1,74 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{B4A1E626-4A48-4977-B291-219882DB3413}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FSharp.VisualStudio.Extension", "src\FSharp.VisualStudio.Extension\FSharp.VisualStudio.Extension.csproj", "{14B9AB0E-2FC0-43F1-9775-20C262202D12}" +EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Compiler.LanguageServer", "src\FSharp.Compiler.LanguageServer\FSharp.Compiler.LanguageServer.fsproj", "{7EDDFB35-2428-4433-8ABF-47233480B746}" +EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Compiler.Service", "src\Compiler\FSharp.Compiler.Service.fsproj", "{0B24DCA6-902F-409E-A18C-7B1BFDDC8849}" +EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Core", "src\FSharp.Core\FSharp.Core.fsproj", "{5A4B66D5-A6C3-402C-A010-7A3635098DA7}" +EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.DependencyManager.Nuget", "src\FSharp.DependencyManager.Nuget\FSharp.DependencyManager.Nuget.fsproj", "{860808CF-D092-4511-B011-6205B654031D}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.Editor", "vsintegration\src\FSharp.Editor\FSharp.Editor.fsproj", "{3D4A95CB-1563-CD9A-3949-FE01922CD5BD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Proto|Any CPU = Proto|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {14B9AB0E-2FC0-43F1-9775-20C262202D12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {14B9AB0E-2FC0-43F1-9775-20C262202D12}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14B9AB0E-2FC0-43F1-9775-20C262202D12}.Proto|Any CPU.ActiveCfg = Release|Any CPU + {14B9AB0E-2FC0-43F1-9775-20C262202D12}.Proto|Any CPU.Build.0 = Release|Any CPU + {14B9AB0E-2FC0-43F1-9775-20C262202D12}.Proto|Any CPU.Deploy.0 = Release|Any CPU + {14B9AB0E-2FC0-43F1-9775-20C262202D12}.Release|Any CPU.ActiveCfg = Release|Any CPU + {14B9AB0E-2FC0-43F1-9775-20C262202D12}.Release|Any CPU.Build.0 = Release|Any CPU + {7EDDFB35-2428-4433-8ABF-47233480B746}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7EDDFB35-2428-4433-8ABF-47233480B746}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7EDDFB35-2428-4433-8ABF-47233480B746}.Proto|Any CPU.ActiveCfg = Release|Any CPU + {7EDDFB35-2428-4433-8ABF-47233480B746}.Proto|Any CPU.Build.0 = Release|Any CPU + {7EDDFB35-2428-4433-8ABF-47233480B746}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7EDDFB35-2428-4433-8ABF-47233480B746}.Release|Any CPU.Build.0 = Release|Any CPU + {0B24DCA6-902F-409E-A18C-7B1BFDDC8849}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B24DCA6-902F-409E-A18C-7B1BFDDC8849}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B24DCA6-902F-409E-A18C-7B1BFDDC8849}.Proto|Any CPU.ActiveCfg = Debug|Any CPU + {0B24DCA6-902F-409E-A18C-7B1BFDDC8849}.Proto|Any CPU.Build.0 = Debug|Any CPU + {0B24DCA6-902F-409E-A18C-7B1BFDDC8849}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B24DCA6-902F-409E-A18C-7B1BFDDC8849}.Release|Any CPU.Build.0 = Release|Any CPU + {5A4B66D5-A6C3-402C-A010-7A3635098DA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5A4B66D5-A6C3-402C-A010-7A3635098DA7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5A4B66D5-A6C3-402C-A010-7A3635098DA7}.Proto|Any CPU.ActiveCfg = Proto|Any CPU + {5A4B66D5-A6C3-402C-A010-7A3635098DA7}.Proto|Any CPU.Build.0 = Proto|Any CPU + {5A4B66D5-A6C3-402C-A010-7A3635098DA7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5A4B66D5-A6C3-402C-A010-7A3635098DA7}.Release|Any CPU.Build.0 = Release|Any CPU + {860808CF-D092-4511-B011-6205B654031D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {860808CF-D092-4511-B011-6205B654031D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {860808CF-D092-4511-B011-6205B654031D}.Proto|Any CPU.ActiveCfg = Debug|Any CPU + {860808CF-D092-4511-B011-6205B654031D}.Proto|Any CPU.Build.0 = Debug|Any CPU + {860808CF-D092-4511-B011-6205B654031D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {860808CF-D092-4511-B011-6205B654031D}.Release|Any CPU.Build.0 = Release|Any CPU + {3D4A95CB-1563-CD9A-3949-FE01922CD5BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3D4A95CB-1563-CD9A-3949-FE01922CD5BD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D4A95CB-1563-CD9A-3949-FE01922CD5BD}.Proto|Any CPU.ActiveCfg = Debug|Any CPU + {3D4A95CB-1563-CD9A-3949-FE01922CD5BD}.Proto|Any CPU.Build.0 = Debug|Any CPU + {3D4A95CB-1563-CD9A-3949-FE01922CD5BD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3D4A95CB-1563-CD9A-3949-FE01922CD5BD}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {14B9AB0E-2FC0-43F1-9775-20C262202D12} = {B4A1E626-4A48-4977-B291-219882DB3413} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BCE01FEF-1B00-471B-81E0-A06994EC90ED} + EndGlobalSection +EndGlobal diff --git a/VisualFSharp.sln b/VisualFSharp.sln index 4f1ab0fa06f..df0b6fe31ce 100644 --- a/VisualFSharp.sln +++ b/VisualFSharp.sln @@ -193,6 +193,14 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "MicroPerf", "tests\benchmar EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroPerfCSharp", "tests\benchmarks\CompiledCodeBenchmarks\MicroPerf\CS\MicroPerfCSharp.csproj", "{9F9DD315-37DA-4413-928E-1CFC6924B64F}" EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Compiler.LanguageServer", "src\FSharp.Compiler.LanguageServer\FSharp.Compiler.LanguageServer.fsproj", "{D72F6593-DB2D-47AC-8E15-8DCE8527972E}" +EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Compiler.LanguageServer.Tests", "tests\FSharp.Compiler.LanguageServer.Tests\FSharp.Compiler.LanguageServer.Tests.fsproj", "{1E83A6C8-FA4D-42BD-B4A5-B7F9AAD1B388}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FSharp.VisualStudio.Extension", "src\FSharp.VisualStudio.Extension\FSharp.VisualStudio.Extension.csproj", "{E1013576-6257-47BA-AAFB-F95B68DAB1FF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CommonLanguageServerProtocol.Framework.Proxy", "src\Microsoft.CommonLanguageServerProtocol.Framework.Proxy\Microsoft.CommonLanguageServerProtocol.Framework.Proxy.csproj", "{FAFF15B5-F7C4-1CE5-9A18-2F6DC93E03ED}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1019,6 +1027,54 @@ Global {9F9DD315-37DA-4413-928E-1CFC6924B64F}.Release|Any CPU.Build.0 = Release|Any CPU {9F9DD315-37DA-4413-928E-1CFC6924B64F}.Release|x86.ActiveCfg = Release|Any CPU {9F9DD315-37DA-4413-928E-1CFC6924B64F}.Release|x86.Build.0 = Release|Any CPU + {D72F6593-DB2D-47AC-8E15-8DCE8527972E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D72F6593-DB2D-47AC-8E15-8DCE8527972E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D72F6593-DB2D-47AC-8E15-8DCE8527972E}.Debug|x86.ActiveCfg = Debug|Any CPU + {D72F6593-DB2D-47AC-8E15-8DCE8527972E}.Debug|x86.Build.0 = Debug|Any CPU + {D72F6593-DB2D-47AC-8E15-8DCE8527972E}.Proto|Any CPU.ActiveCfg = Debug|Any CPU + {D72F6593-DB2D-47AC-8E15-8DCE8527972E}.Proto|Any CPU.Build.0 = Debug|Any CPU + {D72F6593-DB2D-47AC-8E15-8DCE8527972E}.Proto|x86.ActiveCfg = Debug|Any CPU + {D72F6593-DB2D-47AC-8E15-8DCE8527972E}.Proto|x86.Build.0 = Debug|Any CPU + {D72F6593-DB2D-47AC-8E15-8DCE8527972E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D72F6593-DB2D-47AC-8E15-8DCE8527972E}.Release|Any CPU.Build.0 = Release|Any CPU + {D72F6593-DB2D-47AC-8E15-8DCE8527972E}.Release|x86.ActiveCfg = Release|Any CPU + {D72F6593-DB2D-47AC-8E15-8DCE8527972E}.Release|x86.Build.0 = Release|Any CPU + {1E83A6C8-FA4D-42BD-B4A5-B7F9AAD1B388}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1E83A6C8-FA4D-42BD-B4A5-B7F9AAD1B388}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1E83A6C8-FA4D-42BD-B4A5-B7F9AAD1B388}.Debug|x86.ActiveCfg = Debug|Any CPU + {1E83A6C8-FA4D-42BD-B4A5-B7F9AAD1B388}.Debug|x86.Build.0 = Debug|Any CPU + {1E83A6C8-FA4D-42BD-B4A5-B7F9AAD1B388}.Proto|Any CPU.ActiveCfg = Debug|Any CPU + {1E83A6C8-FA4D-42BD-B4A5-B7F9AAD1B388}.Proto|Any CPU.Build.0 = Debug|Any CPU + {1E83A6C8-FA4D-42BD-B4A5-B7F9AAD1B388}.Proto|x86.ActiveCfg = Debug|Any CPU + {1E83A6C8-FA4D-42BD-B4A5-B7F9AAD1B388}.Proto|x86.Build.0 = Debug|Any CPU + {1E83A6C8-FA4D-42BD-B4A5-B7F9AAD1B388}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1E83A6C8-FA4D-42BD-B4A5-B7F9AAD1B388}.Release|Any CPU.Build.0 = Release|Any CPU + {1E83A6C8-FA4D-42BD-B4A5-B7F9AAD1B388}.Release|x86.ActiveCfg = Release|Any CPU + {1E83A6C8-FA4D-42BD-B4A5-B7F9AAD1B388}.Release|x86.Build.0 = Release|Any CPU + {E1013576-6257-47BA-AAFB-F95B68DAB1FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E1013576-6257-47BA-AAFB-F95B68DAB1FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1013576-6257-47BA-AAFB-F95B68DAB1FF}.Debug|x86.ActiveCfg = Debug|Any CPU + {E1013576-6257-47BA-AAFB-F95B68DAB1FF}.Debug|x86.Build.0 = Debug|Any CPU + {E1013576-6257-47BA-AAFB-F95B68DAB1FF}.Proto|Any CPU.ActiveCfg = Debug|Any CPU + {E1013576-6257-47BA-AAFB-F95B68DAB1FF}.Proto|Any CPU.Build.0 = Debug|Any CPU + {E1013576-6257-47BA-AAFB-F95B68DAB1FF}.Proto|x86.ActiveCfg = Debug|Any CPU + {E1013576-6257-47BA-AAFB-F95B68DAB1FF}.Proto|x86.Build.0 = Debug|Any CPU + {E1013576-6257-47BA-AAFB-F95B68DAB1FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E1013576-6257-47BA-AAFB-F95B68DAB1FF}.Release|Any CPU.Build.0 = Release|Any CPU + {E1013576-6257-47BA-AAFB-F95B68DAB1FF}.Release|x86.ActiveCfg = Release|Any CPU + {E1013576-6257-47BA-AAFB-F95B68DAB1FF}.Release|x86.Build.0 = Release|Any CPU + {FAFF15B5-F7C4-1CE5-9A18-2F6DC93E03ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FAFF15B5-F7C4-1CE5-9A18-2F6DC93E03ED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FAFF15B5-F7C4-1CE5-9A18-2F6DC93E03ED}.Debug|x86.ActiveCfg = Debug|Any CPU + {FAFF15B5-F7C4-1CE5-9A18-2F6DC93E03ED}.Debug|x86.Build.0 = Debug|Any CPU + {FAFF15B5-F7C4-1CE5-9A18-2F6DC93E03ED}.Proto|Any CPU.ActiveCfg = Debug|Any CPU + {FAFF15B5-F7C4-1CE5-9A18-2F6DC93E03ED}.Proto|Any CPU.Build.0 = Debug|Any CPU + {FAFF15B5-F7C4-1CE5-9A18-2F6DC93E03ED}.Proto|x86.ActiveCfg = Debug|Any CPU + {FAFF15B5-F7C4-1CE5-9A18-2F6DC93E03ED}.Proto|x86.Build.0 = Debug|Any CPU + {FAFF15B5-F7C4-1CE5-9A18-2F6DC93E03ED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FAFF15B5-F7C4-1CE5-9A18-2F6DC93E03ED}.Release|Any CPU.Build.0 = Release|Any CPU + {FAFF15B5-F7C4-1CE5-9A18-2F6DC93E03ED}.Release|x86.ActiveCfg = Release|Any CPU + {FAFF15B5-F7C4-1CE5-9A18-2F6DC93E03ED}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1099,6 +1155,7 @@ Global {6734FC6F-B5F3-45E1-9A72-720378BB49C9} = {DFB6ADD7-3149-43D9-AFA0-FC4A818B472B} {601CD5C1-EAFA-4AE3-8FB9-F667B5728213} = {DFB6ADD7-3149-43D9-AFA0-FC4A818B472B} {9F9DD315-37DA-4413-928E-1CFC6924B64F} = {DFB6ADD7-3149-43D9-AFA0-FC4A818B472B} + {1E83A6C8-FA4D-42BD-B4A5-B7F9AAD1B388} = {CFE3259A-2D30-4EB0-80D5-E8B5F3D01449} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {48EDBBBE-C8EE-4E3C-8B19-97184A487B37} diff --git a/buildtools/AssemblyCheck/SkipVerifyEmbeddedPdb.txt b/buildtools/AssemblyCheck/SkipVerifyEmbeddedPdb.txt index 7af2a7f58cb..f7d25f2ca25 100644 --- a/buildtools/AssemblyCheck/SkipVerifyEmbeddedPdb.txt +++ b/buildtools/AssemblyCheck/SkipVerifyEmbeddedPdb.txt @@ -3,7 +3,8 @@ FSharp.Benchmarks.Common.dll FSharp.Compiler.Benchmarks.dll FSharp.Compiler.ComponentTests.dll FSharp.Test.Utilities.dll +FSharp.Compiler.LanguageServer.Tests.dll FSharp.Compiler.Private.Scripting.UnitTests.dll FSharp.Compiler.Service.Tests.dll FSharp.Core.UnitTests.dll -FSharpSuite.Tests.dll +FSharpSuite.Tests.dll \ No newline at end of file diff --git a/src/Compiler/FSharp.Compiler.Service.fsproj b/src/Compiler/FSharp.Compiler.Service.fsproj index 6c087fa99fe..3953da89448 100644 --- a/src/Compiler/FSharp.Compiler.Service.fsproj +++ b/src/Compiler/FSharp.Compiler.Service.fsproj @@ -97,6 +97,8 @@ + + diff --git a/src/Compiler/Facilities/AsyncMemoize.fs b/src/Compiler/Facilities/AsyncMemoize.fs index ac52eaf8607..e776fe0aae0 100644 --- a/src/Compiler/Facilities/AsyncMemoize.fs +++ b/src/Compiler/Facilities/AsyncMemoize.fs @@ -114,7 +114,7 @@ module internal Utils = /// Return file name with one directory above it let shortPath (path: string) = - let dirPath = !!Path.GetDirectoryName(path) + let dirPath = Path.GetDirectoryName(path) |> Option.ofObj |> Option.defaultValue "" let dir = dirPath.Split Path.DirectorySeparatorChar diff --git a/src/Compiler/Facilities/Hashing.fs b/src/Compiler/Facilities/Hashing.fs index dd58495c59e..020b50e2663 100644 --- a/src/Compiler/Facilities/Hashing.fs +++ b/src/Compiler/Facilities/Hashing.fs @@ -46,7 +46,11 @@ module internal Md5Hasher = let private md5 = new ThreadLocal<_>(fun () -> System.Security.Cryptography.MD5.Create()) - let computeHash (bytes: byte array) = md5.Value.ComputeHash(bytes) + let computeHash (bytes: byte array) = + // md5.Value.ComputeHash(bytes) TODO: the threadlocal is not working in new VS extension + ignore md5 + let md5 = System.Security.Cryptography.MD5.Create() + md5.ComputeHash(bytes) let empty = Array.empty diff --git a/src/Compiler/Service/FSharpProjectSnapshot.fs b/src/Compiler/Service/FSharpProjectSnapshot.fs index 8ca44143037..91a6efd472a 100644 --- a/src/Compiler/Service/FSharpProjectSnapshot.fs +++ b/src/Compiler/Service/FSharpProjectSnapshot.fs @@ -619,6 +619,7 @@ and [] FSha member _.OriginalLoadReferences = projectSnapshot.OriginalLoadReferences member _.Stamp = projectSnapshot.Stamp member _.OutputFileName = projectSnapshot.OutputFileName + member _.ProjectConfig = projectSnapshot.ProjectConfig static member Create ( @@ -745,6 +746,69 @@ and [] FSha FSharpProjectSnapshot.FromOptions(options, getFileSnapshot) + static member FromResponseFile(responseFile: FileInfo, projectFileName) = + if not responseFile.Exists then + failwith $"%s{responseFile.FullName} does not exist" + + let compilerArgs = File.ReadAllLines responseFile.FullName + + let directoryName: string = + match responseFile.DirectoryName with + | null -> failwith "Directory name of the response file is null" + | str -> str + + FSharpProjectSnapshot.FromCommandLineArgs(compilerArgs, directoryName, projectFileName) + + static member FromCommandLineArgs(compilerArgs: string array, directoryPath: string, projectFileName) = + let fsharpFileExtensions = set [| ".fs"; ".fsi"; ".fsx" |] + + let isFSharpFile (file: string) = + Set.exists (fun (ext: string) -> file.EndsWith(ext, StringComparison.Ordinal)) fsharpFileExtensions + + let isReference: string -> bool = _.StartsWith("-r:") + + let fsharpFiles = + compilerArgs + |> Array.choose (fun (line: string) -> + if not (isFSharpFile line) then + None + else + + let fullPath = Path.Combine(directoryPath, line) + if not (File.Exists fullPath) then None else Some fullPath) + |> Array.toList + + let referencesOnDisk = + compilerArgs |> Seq.filter isReference |> Seq.map _.Substring(3) |> Seq.toList + + let otherOptions = + compilerArgs + |> Seq.filter (not << isReference) + |> Seq.filter (not << isFSharpFile) + |> Seq.toList + + FSharpProjectSnapshot.Create( + projectFileName = projectFileName, + outputFileName = None, + projectId = None, + sourceFiles = (fsharpFiles |> List.map FSharpFileSnapshot.CreateFromFileSystem), + referencesOnDisk = + (referencesOnDisk + |> List.map (fun x -> + { + Path = x + LastModified = FileSystem.GetLastWriteTimeShim(x) + })), + otherOptions = otherOptions, + referencedProjects = [], + isIncompleteTypeCheckEnvironment = false, + useScriptResolutionRules = false, + loadTime = DateTime.Now, + unresolvedReferences = None, + originalLoadReferences = [], + stamp = None + ) + let internal snapshotTable = ConditionalWeakTable() diff --git a/src/Compiler/Service/FSharpWorkspace.fs b/src/Compiler/Service/FSharpWorkspace.fs index ccde4b98fb1..0b557386d07 100644 --- a/src/Compiler/Service/FSharpWorkspace.fs +++ b/src/Compiler/Service/FSharpWorkspace.fs @@ -46,14 +46,7 @@ type FSharpWorkspace(checker: FSharpChecker) = ) ) - member internal this.Debug_DumpMermaid(path) = - let content = - depGraph.Debug_RenderMermaid (function - // Collapse all reference on disk nodes into one. Otherwise the graph is too big to render. - | WorkspaceGraphTypes.WorkspaceNodeKey.ReferenceOnDisk _ -> WorkspaceGraphTypes.WorkspaceNodeKey.ReferenceOnDisk "..." - | x -> x) - - File.WriteAllText(__SOURCE_DIRECTORY__ + path, content) + member internal _.DepGraph = depGraph /// The `FSharpChecker` instance used by this workspace. member _.Checker = checker diff --git a/src/Compiler/Service/FSharpWorkspaceQuery.fs b/src/Compiler/Service/FSharpWorkspaceQuery.fs index b720608b27f..39c87094928 100644 --- a/src/Compiler/Service/FSharpWorkspaceQuery.fs +++ b/src/Compiler/Service/FSharpWorkspaceQuery.fs @@ -5,12 +5,15 @@ module FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery open System open System.Collections.Generic +open FSharp.Compiler.Diagnostics open System.Threading open FSharp.Compiler.CodeAnalysis open Internal.Utilities.DependencyGraph +open Internal.Utilities.Library.Extras open FSharpWorkspaceState +open Internal.Utilities.Library #nowarn "57" @@ -32,13 +35,20 @@ type FSharpWorkspaceQuery internal (depGraph: IThreadSafeDependencyGraph<_, _>, // in order to be able to clear previous diagnostics let getDiagnosticResultId () = Interlocked.Increment(&resultIdCounter) + member internal _.Checker = checker + member _.GetProjectSnapshot projectIdentifier = + use _ = + Activity.start "GetProjectSnapshot" [ Activity.Tags.project, projectIdentifier.ToString() |> (!!) ] + try depGraph.GetProjectSnapshot projectIdentifier |> Some with :? KeyNotFoundException -> None member _.GetProjectSnapshotForFile(file: Uri) = + use _ = + Activity.start "GetProjectSnapshotForFile" [ Activity.Tags.fileName, file.LocalPath ] depGraph.GetProjectsContaining file.LocalPath @@ -47,28 +57,48 @@ type FSharpWorkspaceQuery internal (depGraph: IThreadSafeDependencyGraph<_, _>, // Otherwise we have to keep track of which project/configuration is active |> Seq.tryHead // For now just get the first one - // TODO: split to parse and check diagnostics - member this.GetDiagnosticsForFile(file: Uri) = + member this.GetParseAndCheckResultsForFile(file: Uri) = async { - let! diagnostics = + use _ = + Activity.start "GetParseAndCheckResultsForFile" [ Activity.Tags.fileName, file.LocalPath ] + + return! this.GetProjectSnapshotForFile file |> Option.map (fun snapshot -> async { - let! parseResult, checkFileAnswer = - checker.ParseAndCheckFileInProject(file.LocalPath, snapshot, "LSP Get diagnostics") + let! parseResult, checkFileAnswer = checker.ParseAndCheckFileInProject(file.LocalPath, snapshot) return match checkFileAnswer with - | FSharpCheckFileAnswer.Succeeded result -> result.Diagnostics - | FSharpCheckFileAnswer.Aborted -> parseResult.Diagnostics + | FSharpCheckFileAnswer.Succeeded result -> Some parseResult, Some result + | FSharpCheckFileAnswer.Aborted -> Some parseResult, None }) - |> Option.defaultValue (async.Return [||]) + |> Option.defaultValue (async.Return(None, None)) - return FSharpDiagnosticReport(diagnostics, getDiagnosticResultId ()) } - member this.GetSemanticClassification(file) = + member this.GetCheckResultsForFile(file) = + this.GetParseAndCheckResultsForFile file |> Async.map snd + + // TODO: split to parse and check diagnostics + member this.GetDiagnosticsForFile(file: Uri) = + use _ = + Activity.start "GetDiagnosticsForFile" [ Activity.Tags.fileName, file.LocalPath ] + + this.GetParseAndCheckResultsForFile file + |> Async.map (fun results -> + let diagnostics = + match results with + | _, Some checkResult -> checkResult.Diagnostics + | Some parseResult, _ -> parseResult.Diagnostics + | _ -> [||] + + FSharpDiagnosticReport(diagnostics, getDiagnosticResultId ())) + + member this.GetSemanticClassification(file: Uri) = + use _ = + Activity.start "GetSemanticClassification" [ Activity.Tags.fileName, file.LocalPath ] this.GetProjectSnapshotForFile file |> Option.map (fun snapshot -> diff --git a/src/Compiler/Service/FSharpWorkspaceState.fs b/src/Compiler/Service/FSharpWorkspaceState.fs index b7a3a70dd89..ff112a7977c 100644 --- a/src/Compiler/Service/FSharpWorkspaceState.fs +++ b/src/Compiler/Service/FSharpWorkspaceState.fs @@ -9,8 +9,10 @@ open System.Runtime.CompilerServices open System.Collections.Concurrent open FSharp.Compiler.CodeAnalysis.ProjectSnapshot +open FSharp.Compiler.Diagnostics open Internal.Utilities.Collections open Internal.Utilities.DependencyGraph +open Internal.Utilities.Library #nowarn "57" @@ -200,9 +202,7 @@ module internal WorkspaceDependencyGraphExtensions = [] static member GetProjectReferencesOf(this: IDependencyGraph<_, _>, project) = this.GetDependenciesOf(WorkspaceNodeKey.ProjectWithoutFiles project) - |> Seq.choose (function - | WorkspaceNodeKey.ProjectSnapshot projectId -> Some projectId - | _ -> None) + |> Seq.choose WorkspaceNode.projectSnapshotKey [] static member GetProjectsThatReference(this: IDependencyGraph<_, _>, dllPath) = @@ -216,8 +216,26 @@ module internal WorkspaceDependencyGraphExtensions = [] static member GetSourceFile(this: IDependencyGraph<_, _>, file) = - this.GetValue(WorkspaceNodeKey.SourceFile file) - |> _.Unpack(WorkspaceNode.sourceFile) + this.GetValue(WorkspaceNodeKey.SourceFile file).Unpack(WorkspaceNode.sourceFile) + + [] + static member GetFilesOf(this: IDependencyGraph<_, _>, project) = + this.GetDependenciesOf(WorkspaceNodeKey.ProjectSnapshot project) + |> Seq.map this.GetValue + |> Seq.choose WorkspaceNode.sourceFile + + [] + static member ReplaceSourceFiles(this: IDependencyGraph<_, _>, project, sourceFiles) = + let projectId = WorkspaceNodeKey.ProjectSnapshot project + + this.GetDependenciesOf(WorkspaceNodeKey.ProjectSnapshot project) + |> Seq.where (WorkspaceNode.sourceFileKey >> _.IsSome) + |> Seq.iter (fun fileId -> this.RemoveDependency(projectId, noLongerDependsOn = fileId)) + + sourceFiles + |> Seq.map (fun (file, snapshot) -> WorkspaceNodeKey.SourceFile file, WorkspaceNodeValue.SourceFile snapshot) + |> this.AddList + |> Seq.iter (fun fileId -> this.AddDependency(WorkspaceNodeKey.ProjectSnapshot project, dependsOn = fileId)) /// Interface for managing files in an F# workspace. [] @@ -227,21 +245,33 @@ type FSharpWorkspaceFiles internal (depGraph: IThreadSafeDependencyGraph<_, _>) let openFiles = ConcurrentDictionary() /// Indicates that a file has been opened and has the given content. Any updates to the file should be done through `Files.Edit`. - member this.Open = this.Edit + member _.Open(file: Uri, content) = + use _ = Activity.start "Files.Open" [ Activity.Tags.fileName, file.LocalPath ] + + openFiles.AddOrUpdate(file.LocalPath, content, (fun _ _ -> content)) |> ignore + depGraph.AddOrUpdateFile(file.LocalPath, FSharpFileSnapshot.CreateFromString(file.LocalPath, content)) /// Indicates that a file has been changed and now has the given content. If it wasn't previously open it is considered open now. member _.Edit(file: Uri, content) = + use _ = Activity.start "Files.Edit" [ Activity.Tags.fileName, file.LocalPath ] + openFiles.AddOrUpdate(file.LocalPath, content, (fun _ _ -> content)) |> ignore depGraph.AddOrUpdateFile(file.LocalPath, FSharpFileSnapshot.CreateFromString(file.LocalPath, content)) /// Indicates that a file has been closed. Any changes that were not saved to disk are undone and any further reading /// of the file's contents will be from the filesystem. member _.Close(file: Uri) = + use _ = Activity.start "Files.Close" [ Activity.Tags.fileName, file.LocalPath ] + openFiles.TryRemove(file.LocalPath) |> ignore // The file may have had changes that weren't saved to disk and are therefore undone by closing it. depGraph.AddOrUpdateFile(file.LocalPath, FSharpFileSnapshot.CreateFromFileSystem(file.LocalPath)) + /// Returns file paths for all source files of the given project. + member _.OfProject(projectIdentifier: FSharpProjectIdentifier) = + depGraph.GetFilesOf projectIdentifier |> Seq.map _.FileName + member internal _.GetFileContentIfOpen(path: string) = match openFiles.TryGetValue(path) with | true, content -> Some content @@ -254,8 +284,34 @@ type FSharpWorkspaceProjects internal (depGraph: IThreadSafeDependencyGraph<_, _ /// A map from project output path to project identifier. let outputPathMap = ConcurrentDictionary() + let createFileSnapshot path = + files.GetFileContentIfOpen path + |> Option.map (fun content -> FSharpFileSnapshot.CreateFromString(path, content)) + |> Option.defaultWith (fun () -> FSharpFileSnapshot.CreateFromFileSystem path) + + member val internal Debug_DumpGraphOnEveryChange: string option = None with get, set + + member internal _.files = files + + member internal this.Debug_DumpMermaid(path) = + let content = + depGraph.Debug_RenderMermaid (function + // Collapse all reference on disk nodes into one. Otherwise the graph is too big to render. + | WorkspaceGraphTypes.WorkspaceNodeKey.ReferenceOnDisk _ -> WorkspaceGraphTypes.WorkspaceNodeKey.ReferenceOnDisk "..." + | x -> x) + + File.WriteAllText(path, content) + /// Adds or updates an F# project in the workspace. Project is identified by the project file and output path or FSharpProjectIdentifier. - member _.AddOrUpdate(projectConfig: ProjectConfig, sourceFilePaths: string seq) = + member this.AddOrUpdate(projectConfig: ProjectConfig, sourceFilePaths: string seq) = + + use _ = + Activity.start + "Projects.AddOrUpdate" + [ + Activity.Tags.project, projectConfig.Identifier.ToString() + "sourceFiles", sourceFilePaths |> String.concat "; " + ] let projectIdentifier = projectConfig.Identifier @@ -293,14 +349,7 @@ type FSharpWorkspaceProjects internal (depGraph: IThreadSafeDependencyGraph<_, _ projectConfig, referencedProjects) ) - .AddSourceFiles( - sourceFilePaths - |> Seq.map (fun path -> - path, - files.GetFileContentIfOpen path - |> Option.map (fun content -> FSharpFileSnapshot.CreateFromString(path, content)) - |> Option.defaultWith (fun () -> FSharpFileSnapshot.CreateFromFileSystem path)) - ) + .AddSourceFiles(sourceFilePaths |> Seq.map (fun path -> path, createFileSnapshot path)) .AddProjectSnapshot( (fun ((projectConfig, referencedProjects), sourceFiles) -> ProjectSnapshot(projectConfig, referencedProjects, sourceFiles |> Seq.toList) @@ -326,8 +375,13 @@ type FSharpWorkspaceProjects internal (depGraph: IThreadSafeDependencyGraph<_, _ for dependentProjectId in dependentProjectIds do depGraph.AddProjectReference(dependentProjectId, projectIdentifier) + this.Debug_DumpGraphOnEveryChange |> Option.iter (this.Debug_DumpMermaid) + projectIdentifier) + member this.AddOrUpdate(projectConfig, sourceFilePaths: Uri seq) = + this.AddOrUpdate(projectConfig, sourceFilePaths |> Seq.map _.LocalPath) + member this.AddOrUpdate(projectPath: string, outputPath, compilerArgs) = let directoryPath = @@ -336,7 +390,9 @@ type FSharpWorkspaceProjects internal (depGraph: IThreadSafeDependencyGraph<_, _ let fsharpFileExtensions = set [| ".fs"; ".fsi"; ".fsx" |] let isFSharpFile (file: string) = - Set.exists (fun (ext: string) -> file.EndsWith(ext, StringComparison.Ordinal)) fsharpFileExtensions + file.Length > 0 + && file[0] <> '-' + && Set.exists (fun (ext: string) -> file.EndsWith(ext, StringComparison.Ordinal)) fsharpFileExtensions let isReference: string -> bool = _.StartsWith("-r:") @@ -365,3 +421,31 @@ type FSharpWorkspaceProjects internal (depGraph: IThreadSafeDependencyGraph<_, _ ProjectConfig(projectFileName, Some outputFileName, referencesOnDisk, otherOptions) this.AddOrUpdate(projectConfig, sourceFiles) + + member this.Update(projectIdentifier, newSourceFiles) = + + let newSourceFiles = newSourceFiles |> Seq.cache + + use _ = + Activity.start + "Projects.Update" + [ + Activity.Tags.project, !!projectIdentifier.ToString() + "sourceFiles", newSourceFiles |> String.concat "; " + ] + + depGraph.Transact(fun depGraph -> + + let existingSourceFiles = + depGraph.GetFilesOf projectIdentifier |> Seq.map (fun f -> f.FileName, f) |> Map + + let newFilesWithSnapshots = + newSourceFiles + |> Seq.map (fun path -> + path, + existingSourceFiles.TryFind path + |> Option.defaultWith (fun () -> createFileSnapshot path)) + + depGraph.ReplaceSourceFiles(projectIdentifier, newFilesWithSnapshots) + + this.Debug_DumpGraphOnEveryChange |> Option.iter (this.Debug_DumpMermaid)) diff --git a/src/Compiler/Service/TransparentCompiler.fs b/src/Compiler/Service/TransparentCompiler.fs index 33e98037f00..0c3f01d4a3a 100644 --- a/src/Compiler/Service/TransparentCompiler.fs +++ b/src/Compiler/Service/TransparentCompiler.fs @@ -1770,6 +1770,12 @@ type internal TransparentCompiler caches.ProjectExtras.Get( projectSnapshot.SignatureKey, async { + use _ = + Activity.start + "ComputeProjectExtras" + [| + Activity.Tags.project, projectSnapshot.ProjectFileName |> Path.GetFileName |> (!!) + |] let! results, finalInfo, parseDiagnostics = ComputeParseAndCheckAllFilesInProject bootstrapInfo projectSnapshot @@ -1863,6 +1869,13 @@ type internal TransparentCompiler caches.AssemblyData.Get( projectSnapshot.SignatureKey, async { + use _ = + Activity.start + "ComputeAssemblyData" + [| + Activity.Tags.project, projectSnapshot.ProjectFileName |> Path.GetFileName |> (!!) + |] + use! _holder = Cancellable.UseToken() try diff --git a/src/Compiler/Utilities/DependencyGraph.fs b/src/Compiler/Utilities/DependencyGraph.fs index 55eb5e0d746..1156018e994 100644 --- a/src/Compiler/Utilities/DependencyGraph.fs +++ b/src/Compiler/Utilities/DependencyGraph.fs @@ -33,6 +33,7 @@ type IDependencyGraph<'Id, 'Val when 'Id: equality> = abstract member RemoveDependency: node: 'Id * noLongerDependsOn: 'Id -> unit abstract member UpdateNode: id: 'Id * update: ('Val -> 'Val) -> unit abstract member RemoveNode: id: 'Id -> unit + abstract member Debug_GetNodes: ('Id -> bool) -> DependencyNode<'Id, 'Val> seq abstract member Debug_RenderMermaid: ?mapping: ('Id -> 'Id) -> string abstract member OnWarning: (string -> unit) -> unit @@ -193,6 +194,9 @@ module Internal = | false, _ -> () | false, _ -> () + member this.Debug_GetNodes(predicate: 'Id -> bool) : DependencyNode<'Id, 'Val> seq = + nodes.Values |> Seq.filter (fun node -> predicate node.Id) + member _.Debug_RenderMermaid(?mapping) = let mapping = defaultArg mapping id @@ -224,6 +228,8 @@ module Internal = interface IDependencyGraph<'Id, 'Val> with + member this.Debug_GetNodes(predicate) = self.Debug_GetNodes(predicate) + member _.AddOrUpdateNode(id, value) = self.AddOrUpdateNode(id, value) member _.AddList(nodes) = self.AddList(nodes) @@ -310,6 +316,9 @@ type LockOperatedDependencyGraph<'Id, 'Val when 'Id: equality and 'Id: not null> member _.OnWarning(f) = lock lockObj (fun () -> graph.OnWarning f) + member _.Debug_GetNodes(predicate) = + lock lockObj (fun () -> graph.Debug_GetNodes(predicate)) + member _.Debug_RenderMermaid(m) = lock lockObj (fun () -> graph.Debug_RenderMermaid(?mapping = m)) diff --git a/src/Compiler/Utilities/lib.fs b/src/Compiler/Utilities/lib.fs index 8acde600ec4..e3b08d3c98f 100755 --- a/src/Compiler/Utilities/lib.fs +++ b/src/Compiler/Utilities/lib.fs @@ -24,7 +24,7 @@ let isEnvVarSet s = let GetEnvInteger e dflt = match Environment.GetEnvironmentVariable(e) with null -> dflt | t -> try int t with _ -> dflt -let dispose (x: IDisposable MaybeNull) = +let dispose (x: IDisposable MaybeNull) = match x with | Null -> () | NonNull x -> x.Dispose() @@ -451,4 +451,10 @@ module ListParallel = |> ArrayParallel.map f |> Array.toList - \ No newline at end of file +[] +module Async = + let map f a = + async { + let! a = a + return f a + } diff --git a/src/Compiler/Utilities/lib.fsi b/src/Compiler/Utilities/lib.fsi index 5d82031642f..566d6779148 100644 --- a/src/Compiler/Utilities/lib.fsi +++ b/src/Compiler/Utilities/lib.fsi @@ -291,3 +291,7 @@ module ListParallel = val map: ('T -> 'U) -> 'T list -> 'U list //val inline mapi: (int -> 'T -> 'U) -> 'T list -> 'U list + +[] +module Async = + val map: ('T -> 'U) -> Async<'T> -> Async<'U> diff --git a/src/FSharp.Compiler.LanguageServer/Common/CapabilitiesManager.fs b/src/FSharp.Compiler.LanguageServer/Common/CapabilitiesManager.fs new file mode 100644 index 00000000000..556a1d96edc --- /dev/null +++ b/src/FSharp.Compiler.LanguageServer/Common/CapabilitiesManager.fs @@ -0,0 +1,63 @@ +namespace FSharp.Compiler.LanguageServer.Common + +open Microsoft.VisualStudio.LanguageServer.Protocol +open Microsoft.CommonLanguageServerProtocol.Framework +open FSharp.Compiler.LanguageServer + +type IServerCapabilitiesOverride = + abstract member OverrideServerCapabilities: FSharpLanguageServerConfig * ServerCapabilities * ClientCapabilities -> ServerCapabilities + +type CapabilitiesManager(config: FSharpLanguageServerConfig, scOverrides: IServerCapabilitiesOverride seq) = + + let mutable initializeParams = None + + let getInitializeParams () = + match initializeParams with + | Some params' -> params' + | None -> failwith "InitializeParams is null" + + let addIf (enabled: bool) (capability: 'a) = + if enabled then capability |> withNull else null + + let defaultCapabilities (_clientCapabilities: ClientCapabilities) = + // TODO: don't register if dynamic registraion is supported + ServerCapabilities( + TextDocumentSync = TextDocumentSyncOptions(OpenClose = true, Change = TextDocumentSyncKind.Full), + DiagnosticOptions = + addIf + config.EnabledFeatures.Diagnostics + (DiagnosticOptions( + WorkDoneProgress = true, + InterFileDependencies = true, + Identifier = "potato", + WorkspaceDiagnostics = true + )), + //CompletionProvider = CompletionOptions(TriggerCharacters = [| "."; " " |], ResolveProvider = true, WorkDoneProgress = true), + //HoverProvider = SumType(HoverOptions(WorkDoneProgress = true)) + SemanticTokensOptions = + addIf + config.EnabledFeatures.SemanticHighlighting + + (SemanticTokensOptions( + Legend = + SemanticTokensLegend( + TokenTypes = (SemanticTokenTypes.AllTypes |> Seq.toArray), // XXX should be extended + TokenModifiers = (SemanticTokenModifiers.AllModifiers |> Seq.toArray) + ), + Range = false + )) + ) + + interface IInitializeManager with + member this.SetInitializeParams(request) = initializeParams <- Some request + + member this.GetInitializeParams() = getInitializeParams () + + member this.GetInitializeResult() = + let clientCapabilities = getInitializeParams().Capabilities + + let serverCapabilities = + (defaultCapabilities clientCapabilities, scOverrides) + ||> Seq.fold (fun acc (x: IServerCapabilitiesOverride) -> x.OverrideServerCapabilities(config, acc, clientCapabilities)) + + InitializeResult(Capabilities = serverCapabilities) diff --git a/src/FSharp.Compiler.LanguageServer/Common/FSharpRequestContext.fs b/src/FSharp.Compiler.LanguageServer/Common/FSharpRequestContext.fs new file mode 100644 index 00000000000..fd096d3e582 --- /dev/null +++ b/src/FSharp.Compiler.LanguageServer/Common/FSharpRequestContext.fs @@ -0,0 +1,187 @@ +namespace FSharp.Compiler.LanguageServer.Common + +open Microsoft.CommonLanguageServerProtocol.Framework +open System.Threading +open System.Threading.Tasks + +open System +open System.Collections.Generic +open Microsoft.VisualStudio.LanguageServer.Protocol + +open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.EditorServices +open FSharp.Compiler.Tokenization + +open System.Threading +open FSharp.Compiler.CodeAnalysis.Workspace + +#nowarn "57" + +module TokenTypes = + + [] + let (|LexicalClassification|_|) (tok: FSharpToken) = + if tok.IsKeyword then + ValueSome SemanticTokenTypes.Keyword + elif tok.IsNumericLiteral then + ValueSome SemanticTokenTypes.Number + elif tok.IsCommentTrivia then + ValueSome SemanticTokenTypes.Comment + elif tok.IsStringLiteral then + ValueSome SemanticTokenTypes.String + else + ValueNone + + // Tokenizes the source code and returns a list of token ranges and their SemanticTokenTypes + let GetSyntacticTokenTypes (source: FSharp.Compiler.Text.ISourceText) (fileName: string) = + let mutable tokRangesAndTypes = [] + + let tokenCallback = + fun (tok: FSharpToken) -> + match tok with + | LexicalClassification tokType -> tokRangesAndTypes <- (tok.Range, tokType) :: tokRangesAndTypes + | _ -> () + + FSharpLexer.Tokenize( + source, + tokenCallback, + flags = + (FSharpLexerFlags.Default + &&& ~~~FSharpLexerFlags.Compiling + &&& ~~~FSharpLexerFlags.UseLexFilter), + filePath = fileName + ) + + tokRangesAndTypes + + let FSharpTokenTypeToLSP (fst: SemanticClassificationType) = + // XXX kinda arbitrary mapping + match fst with + | SemanticClassificationType.ReferenceType -> SemanticTokenTypes.Class + | SemanticClassificationType.ValueType -> SemanticTokenTypes.Struct + | SemanticClassificationType.UnionCase -> SemanticTokenTypes.Enum + | SemanticClassificationType.UnionCaseField -> SemanticTokenTypes.EnumMember + | SemanticClassificationType.Function -> SemanticTokenTypes.Function + | SemanticClassificationType.Property -> SemanticTokenTypes.Property + | SemanticClassificationType.Module -> SemanticTokenTypes.Type + | SemanticClassificationType.Namespace -> SemanticTokenTypes.Namespace + | SemanticClassificationType.Interface -> SemanticTokenTypes.Interface + | SemanticClassificationType.TypeArgument -> SemanticTokenTypes.TypeParameter + | SemanticClassificationType.Operator -> SemanticTokenTypes.Operator + | SemanticClassificationType.Method -> SemanticTokenTypes.Method + | SemanticClassificationType.ExtensionMethod -> SemanticTokenTypes.Method + | SemanticClassificationType.Field -> SemanticTokenTypes.Property + | SemanticClassificationType.Event -> SemanticTokenTypes.Event + | SemanticClassificationType.Delegate -> SemanticTokenTypes.Function + | SemanticClassificationType.NamedArgument -> SemanticTokenTypes.Parameter + | SemanticClassificationType.LocalValue -> SemanticTokenTypes.Variable + | SemanticClassificationType.Plaintext -> SemanticTokenTypes.String + | SemanticClassificationType.Type -> SemanticTokenTypes.Type + | SemanticClassificationType.Printf -> SemanticTokenTypes.Keyword + | _ -> SemanticTokenTypes.Comment + + let toIndex (x: string) = + SemanticTokenTypes.AllTypes |> Seq.findIndex (fun y -> y = x) + +type FSharpRequestContext(lspServices: ILspServices, logger: ILspLogger, workspace: FSharpWorkspace) = + member _.LspServices = lspServices + member _.Logger = logger + member _.Workspace = workspace + + member this.GetSemanticTokensForFile(file) = + task { + let! scv = this.Workspace.Query.GetSemanticClassification file + let! source = this.Workspace.Query.GetSource file + + match scv, source with + | Some view, Some source -> + + let tokens = ResizeArray() + + view.ForEach(fun item -> + let range = item.Range + let tokenType = item.Type |> TokenTypes.FSharpTokenTypeToLSP |> TokenTypes.toIndex + tokens.Add(range, tokenType)) + + let syntacticClassifications = + TokenTypes.GetSyntacticTokenTypes source file.LocalPath + |> Seq.map (fun (r, t) -> (r, TokenTypes.toIndex t)) + + let allTokens = Seq.append tokens syntacticClassifications + + let lspFormatTokens = + allTokens + |> Seq.map (fun (r, tokType) -> + let length = r.EndColumn - r.StartColumn // XXX Does not deal with multiline tokens? + + {| + startLine = r.StartLine - 1 + startCol = r.StartColumn + length = length + tokType = tokType + tokMods = 0 + |}) + //(startLine, startCol, length, tokType, tokMods)) + |> Seq.sortWith (fun x1 x2 -> + let c = x1.startLine.CompareTo(x2.startLine) + if c <> 0 then c else x1.startCol.CompareTo(x2.startCol)) + + let tokensRelative = + lspFormatTokens + |> Seq.append + [| + {| + startLine = 0 + startCol = 0 + length = 0 + tokType = 0 + tokMods = 0 + |} + |] + |> Seq.pairwise + |> Seq.map (fun (prev, this) -> + {| + startLine = this.startLine - prev.startLine + startCol = + (if prev.startLine = this.startLine then + this.startCol - prev.startCol + else + this.startCol) + length = this.length + tokType = this.tokType + tokMods = this.tokMods + |}) + + return + tokensRelative + |> Seq.map (fun tok -> [| tok.startLine; tok.startCol; tok.length; tok.tokType; tok.tokMods |]) + |> Seq.concat + |> Seq.toArray + + | _ -> return [||] + } + +type ContextHolder(workspace, lspServices: ILspServices) = + + let logger = lspServices.GetRequiredService() + + let context = FSharpRequestContext(lspServices, logger, workspace) + + member _.GetContext() = context + + member _.UpdateWorkspace(f) = f context.Workspace + +type FShapRequestContextFactory(lspServices: ILspServices) = + + inherit AbstractRequestContextFactory() + + override _.CreateRequestContextAsync<'TRequestParam> + ( + _queueItem: IQueueItem, + _methodHandler: IMethodHandler, + _requestParam: 'TRequestParam, + _cancellationToken: CancellationToken + ) = + lspServices.GetRequiredService() + |> _.GetContext() + |> Task.FromResult diff --git a/src/FSharp.Compiler.LanguageServer/Common/LifecycleManager.fs b/src/FSharp.Compiler.LanguageServer/Common/LifecycleManager.fs new file mode 100644 index 00000000000..1919512375e --- /dev/null +++ b/src/FSharp.Compiler.LanguageServer/Common/LifecycleManager.fs @@ -0,0 +1,45 @@ +namespace FSharp.Compiler.LanguageServer.Common + +open Microsoft.CommonLanguageServerProtocol.Framework +open System +open StreamJsonRpc +open System.Threading.Tasks +open Microsoft.Extensions.DependencyInjection + +#nowarn "3261" + +type LspServiceLifeCycleManager() = + + interface ILifeCycleManager with + member _.ShutdownAsync(_message: string) = + task { + try + printfn "Shutting down" + with + | :? ObjectDisposedException + | :? ConnectionLostException -> () + } + + member _.ExitAsync() = Task.CompletedTask + +type FSharpLspServices(serviceCollection: IServiceCollection) as this = + + do serviceCollection.AddSingleton(this) |> ignore + + let serviceProvider = serviceCollection.BuildServiceProvider() + + interface ILspServices with + member _.GetRequiredService() = serviceProvider.GetRequiredService() + + member _.GetService() = serviceProvider.GetService() + + member _.GetRequiredServices() = serviceProvider.GetServices() + + member _.Dispose() = serviceProvider.Dispose() + + member _.TryGetService(``type``, service) = + match serviceProvider.GetService(``type``) with + | NonNull x -> + service <- x + true + | Null -> false diff --git a/src/FSharp.Compiler.LanguageServer/Executable.fs b/src/FSharp.Compiler.LanguageServer/Executable.fs new file mode 100644 index 00000000000..acc8e869fdc --- /dev/null +++ b/src/FSharp.Compiler.LanguageServer/Executable.fs @@ -0,0 +1,21 @@ +module FSharp.Compiler.LanguageServer.Executable + +open System +open StreamJsonRpc + +[] +let main _argv = + + let jsonRpc = new JsonRpc(Console.OpenStandardOutput(), Console.OpenStandardInput()) + + let _s = new FSharpLanguageServer(jsonRpc, (LspLogger Console.Out.Write)) + + jsonRpc.StartListening() + + async { + while true do + do! Async.Sleep 1000 + } + |> Async.RunSynchronously + + 0 diff --git a/src/FSharp.Compiler.LanguageServer/FSharp.Compiler.LanguageServer.fsproj b/src/FSharp.Compiler.LanguageServer/FSharp.Compiler.LanguageServer.fsproj new file mode 100644 index 00000000000..d6ebfc1c6f6 --- /dev/null +++ b/src/FSharp.Compiler.LanguageServer/FSharp.Compiler.LanguageServer.fsproj @@ -0,0 +1,52 @@ + + + + Exe + net8.0 + true + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/FSharp.Compiler.LanguageServer/FSharpLanguageServer.fs b/src/FSharp.Compiler.LanguageServer/FSharpLanguageServer.fs new file mode 100644 index 00000000000..3aadb29b2b7 --- /dev/null +++ b/src/FSharp.Compiler.LanguageServer/FSharpLanguageServer.fs @@ -0,0 +1,116 @@ +namespace FSharp.Compiler.LanguageServer + +open System.Runtime.CompilerServices +open FSharp.Compiler.LanguageServer.Common +open FSharp.Compiler.LanguageServer.Handlers + +open System +open Microsoft.CommonLanguageServerProtocol.Framework.Handlers +open Microsoft.CommonLanguageServerProtocol.Framework +open Microsoft.Extensions.DependencyInjection +open Microsoft.VisualStudio.LanguageServer.Protocol + +open StreamJsonRpc +open Nerdbank.Streams +open System.Diagnostics +open FSharp.Compiler.CodeAnalysis.Workspace + +#nowarn "57" + +[] +module Stuff = + [] + let FSharpLanguageName = "F#" + +[] +type Extensions = + + [] + static member Please(this: Async<'t>, ct) = + Async.StartAsTask(this, cancellationToken = ct) + +type FSharpLanguageServer + ( + jsonRpc: JsonRpc, + logger: ILspLogger, + ?initialWorkspace: FSharpWorkspace, + ?addExtraHandlers: Action, + ?config: FSharpLanguageServerConfig + ) = + + // TODO: Switch to SystemTextJsonLanguageServer + inherit NewtonsoftLanguageServer(jsonRpc, Newtonsoft.Json.JsonSerializer.CreateDefault(), logger) + + let config = defaultArg config FSharpLanguageServerConfig.Default + let initialWorkspace = defaultArg initialWorkspace (FSharpWorkspace()) + + do + // This spins up the queue and ensure the LSP is ready to start receiving requests + base.Initialize() + + member _.JsonRpc: JsonRpc = jsonRpc + + override this.ConstructLspServices() = + let serviceCollection = new ServiceCollection() + + let _ = + serviceCollection + .AddSingleton(initialWorkspace) + .AddSingleton() + .AddSingleton(config) + .AddSingleton>() + .AddSingleton>() + .AddSingleton() + .AddSingleton() + .AddSingleton(logger) + .AddSingleton, FShapRequestContextFactory>() + .AddSingleton, CapabilitiesManager>() + .AddSingleton(this) + .AddSingleton(new LspServiceLifeCycleManager()) + + match addExtraHandlers with + | Some handler -> handler.Invoke(serviceCollection) + | None -> () + + let lspServices = new FSharpLspServices(serviceCollection) + + lspServices :> ILspServices + + static member Create() = + FSharpLanguageServer.Create(FSharpWorkspace()) + + static member Create(initialWorkspace) = + FSharpLanguageServer.Create(initialWorkspace, (fun _ -> ())) + + static member Create(initialWorkspace, addExtraHandlers: Action) = + FSharpLanguageServer.Create(LspLogger System.Diagnostics.Trace.TraceInformation, initialWorkspace, addExtraHandlers) + + static member Create(initialWorkspace, config: FSharpLanguageServerConfig, addExtraHandlers: Action) = + FSharpLanguageServer.Create(LspLogger System.Diagnostics.Trace.TraceInformation, initialWorkspace, addExtraHandlers, config) + + static member Create + (logger: ILspLogger, initialWorkspace, ?addExtraHandlers: Action, ?config: FSharpLanguageServerConfig) + = + + let struct (clientStream, serverStream) = FullDuplexStream.CreatePair() + + // TODO: handle disposal of these + let formatter = new JsonMessageFormatter() + + let messageHandler = + new HeaderDelimitedMessageHandler(serverStream, serverStream, formatter) + + let jsonRpc = new JsonRpc(messageHandler) + + let listener = new TextWriterTraceListener(Console.Out) + + jsonRpc.TraceSource.Listeners.Add(listener) |> ignore + + jsonRpc.TraceSource.Switch.Level <- SourceLevels.All + + let server = + new FSharpLanguageServer(jsonRpc, logger, initialWorkspace, ?addExtraHandlers = addExtraHandlers, ?config = config) + + jsonRpc.StartListening() + + (clientStream, clientStream), server diff --git a/src/FSharp.Compiler.LanguageServer/FSharpLanguageServerConfig.fs b/src/FSharp.Compiler.LanguageServer/FSharpLanguageServerConfig.fs new file mode 100644 index 00000000000..a1acd2b88da --- /dev/null +++ b/src/FSharp.Compiler.LanguageServer/FSharpLanguageServerConfig.fs @@ -0,0 +1,23 @@ +namespace FSharp.Compiler.LanguageServer + +type FSharpLanguageServerFeatures = + { + Diagnostics: bool + SemanticHighlighting: bool + } + + static member Default = + { + Diagnostics = true + SemanticHighlighting = true + } + +type FSharpLanguageServerConfig = + { + EnabledFeatures: FSharpLanguageServerFeatures + } + + static member Default = + { + EnabledFeatures = FSharpLanguageServerFeatures.Default + } diff --git a/src/FSharp.Compiler.LanguageServer/Handlers/DocumentStateHandler.fs b/src/FSharp.Compiler.LanguageServer/Handlers/DocumentStateHandler.fs new file mode 100644 index 00000000000..b7f281ed9e8 --- /dev/null +++ b/src/FSharp.Compiler.LanguageServer/Handlers/DocumentStateHandler.fs @@ -0,0 +1,47 @@ +namespace FSharp.Compiler.LanguageServer.Handlers + +open Microsoft.CommonLanguageServerProtocol.Framework +open Microsoft.VisualStudio.LanguageServer.Protocol +open FSharp.Compiler.LanguageServer.Common + +open System.Threading +open System.Threading.Tasks + +#nowarn "57" + +type DocumentStateHandler() = + interface IMethodHandler with + member _.MutatesSolutionState = true + + interface IRequestHandler with + [] + member _.HandleRequestAsync + (request: DidOpenTextDocumentParams, context: FSharpRequestContext, _cancellationToken: CancellationToken) + = + let contextHolder = context.LspServices.GetRequiredService() + + contextHolder.UpdateWorkspace _.Files.Open(request.TextDocument.Uri, request.TextDocument.Text) + + Task.FromResult(SemanticTokensDeltaPartialResult()) + + interface IRequestHandler with + [] + member _.HandleRequestAsync + (request: DidChangeTextDocumentParams, context: FSharpRequestContext, _cancellationToken: CancellationToken) + = + let contextHolder = context.LspServices.GetRequiredService() + + contextHolder.UpdateWorkspace _.Files.Edit(request.TextDocument.Uri, request.ContentChanges.[0].Text) + + Task.FromResult(SemanticTokensDeltaPartialResult()) + + interface INotificationHandler with + [] + member _.HandleNotificationAsync + (request: DidCloseTextDocumentParams, context: FSharpRequestContext, _cancellationToken: CancellationToken) + = + let contextHolder = context.LspServices.GetRequiredService() + + contextHolder.UpdateWorkspace _.Files.Close(request.TextDocument.Uri) + + Task.CompletedTask diff --git a/src/FSharp.Compiler.LanguageServer/Handlers/LanguageFeaturesHandler.fs b/src/FSharp.Compiler.LanguageServer/Handlers/LanguageFeaturesHandler.fs new file mode 100644 index 00000000000..474758924d0 --- /dev/null +++ b/src/FSharp.Compiler.LanguageServer/Handlers/LanguageFeaturesHandler.fs @@ -0,0 +1,59 @@ +namespace FSharp.Compiler.LanguageServer.Handlers + +open Microsoft.CommonLanguageServerProtocol.Framework +open Microsoft.VisualStudio.LanguageServer.Protocol +open Microsoft.VisualStudio.FSharp.Editor.CancellableTasks +open FSharp.Compiler.LanguageServer.Common +open FSharp.Compiler.LanguageServer +open System.Threading.Tasks +open System.Threading +open System.Collections.Generic +open Microsoft.VisualStudio.FSharp.Editor + +#nowarn "57" + +type LanguageFeaturesHandler() = + interface IMethodHandler with + member _.MutatesSolutionState = false + + interface IRequestHandler< + DocumentDiagnosticParams, + SumType, + FSharpRequestContext + > with + [] + member _.HandleRequestAsync + (request: DocumentDiagnosticParams, context: FSharpRequestContext, cancellationToken: CancellationToken) + = + cancellableTask { + + let! fsharpDiagnosticReport = context.Workspace.Query.GetDiagnosticsForFile request.TextDocument.Uri + + let report = + FullDocumentDiagnosticReport( + Items = (fsharpDiagnosticReport.Diagnostics |> Array.map (_.ToLspDiagnostic())), + ResultId = fsharpDiagnosticReport.ResultId + ) + + let relatedDocuments = Dictionary() + + relatedDocuments.Add( + request.TextDocument.Uri, + SumType report + ) + + return + SumType( + RelatedFullDocumentDiagnosticReport(RelatedDocuments = relatedDocuments) + ) + } + |> CancellableTask.start cancellationToken + + interface IRequestHandler with + [] + member _.HandleRequestAsync(request: SemanticTokensParams, context: FSharpRequestContext, cancellationToken: CancellationToken) = + cancellableTask { + let! tokens = context.GetSemanticTokensForFile(request.TextDocument.Uri) + return SemanticTokens(Data = tokens) + } + |> CancellableTask.start cancellationToken diff --git a/src/FSharp.Compiler.LanguageServer/Utils.fs b/src/FSharp.Compiler.LanguageServer/Utils.fs new file mode 100644 index 00000000000..c5283b6fb76 --- /dev/null +++ b/src/FSharp.Compiler.LanguageServer/Utils.fs @@ -0,0 +1,90 @@ +namespace FSharp.Compiler.LanguageServer + +open Microsoft.CommonLanguageServerProtocol.Framework +open Microsoft.VisualStudio.LanguageServer.Protocol + +open System.Diagnostics + +open FSharp.Compiler.Diagnostics +open System.Runtime.CompilerServices + +[] +module Utils = + + type LspRange = Microsoft.VisualStudio.LanguageServer.Protocol.Range + + let LspLogger (output: string -> unit) = + { new ILspLogger with + member this.LogEndContext(message: string, ``params``: obj array) : unit = + output $"EndContext :: {message} %A{``params``}" + + member this.LogError(message: string, ``params``: obj array) : unit = + output $"ERROR :: {message} %A{``params``}" + + member this.LogException(``exception``: exn, message: string, ``params``: obj array) : unit = + output $"EXCEPTION :: %A{``exception``} {message} %A{``params``}" + + member this.LogInformation(message: string, ``params``: obj array) : unit = + output $"INFO :: {message} %A{``params``}" + + member this.LogStartContext(message: string, ``params``: obj array) : unit = + output $"StartContext :: {message} %A{``params``}" + + member this.LogWarning(message: string, ``params``: obj array) : unit = + output $"WARNING :: {message} %A{``params``}" + } + + type FSharp.Compiler.Text.Range with + + member this.ToLspRange() = + LspRange( + Start = Position(Line = this.StartLine - 1, Character = this.StartColumn), + End = Position(Line = this.EndLine - 1, Character = this.EndColumn) + ) + +[] +type FSharpDiagnosticExtensions = + + [] + static member ToLspDiagnostic(this: FSharpDiagnostic) = + Diagnostic( + Range = this.Range.ToLspRange(), + Severity = DiagnosticSeverity.Error, + Message = $"LSP: {this.Message}", + //Source = "Intellisense", + Code = SumType this.ErrorNumberText + ) + +module Activity = + let listen (filter) logMsg = + let indent (activity: Activity) = + let rec loop (activity: Activity) n = + match activity.Parent with + | Null -> n + | NonNull parent -> loop (parent) (n + 1) + + String.replicate (loop activity 0) " " + + let collectTags (activity: Activity) = + [ for tag in activity.Tags -> $"{tag.Key}: %A{tag.Value}" ] + |> String.concat ", " + + let listener = + new ActivityListener( + ShouldListenTo = (fun source -> source.Name = FSharp.Compiler.Diagnostics.ActivityNames.FscSourceName), + Sample = + (fun context -> + if filter context.Name then + ActivitySamplingResult.AllDataAndRecorded + else + ActivitySamplingResult.None), + ActivityStarted = (fun a -> logMsg $"{indent a}{a.OperationName} {collectTags a}") + ) + + ActivitySource.AddActivityListener(listener) + + let listenToAll () = + listen (fun _ -> true) Trace.TraceInformation + + let listenToSome () = + listen (fun x -> not <| x.Contains "StackGuard") Trace.TraceInformation diff --git a/src/FSharp.VisualStudio.Extension/.vsextension/string-resources.json b/src/FSharp.VisualStudio.Extension/.vsextension/string-resources.json new file mode 100644 index 00000000000..5f1633293d7 --- /dev/null +++ b/src/FSharp.VisualStudio.Extension/.vsextension/string-resources.json @@ -0,0 +1,4 @@ +{ + "FSharpLspExtension.FSharpLanguageServerProvider.DisplayName": "FSharp Analyzer LSP server" + +} diff --git a/src/FSharp.VisualStudio.Extension/ExtensionEntrypoint.cs b/src/FSharp.VisualStudio.Extension/ExtensionEntrypoint.cs new file mode 100644 index 00000000000..ba47320b925 --- /dev/null +++ b/src/FSharp.VisualStudio.Extension/ExtensionEntrypoint.cs @@ -0,0 +1,34 @@ +namespace FSharp.VisualStudio.Extension; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.Extensibility; +using System.Threading; +using System; +using Extension = Microsoft.VisualStudio.Extensibility.Extension; + +/// +/// Extension entrypoint for the VisualStudio.Extensibility extension. +/// +[VisualStudioContribution] +internal class ExtensionEntrypoint : Extension +{ + + /// + public override ExtensionConfiguration ExtensionConfiguration => new() + { + Metadata = new( + id: "FSharp.VisualStudio.Extension.4fd40904-7bdd-40b0-82ab-588cbee624d1", + version: this.ExtensionAssemblyVersion, + publisherName: "Publisher name", + displayName: "FSharp.VisualStudio.Extension", + description: "Extension description"), + }; + + /// + protected override void InitializeServices(IServiceCollection serviceCollection) + { + base.InitializeServices(serviceCollection); + + // You can configure dependency injection here by adding services to the serviceCollection. + } +} diff --git a/src/FSharp.VisualStudio.Extension/FSharp.VisualStudio.Extension.csproj b/src/FSharp.VisualStudio.Extension/FSharp.VisualStudio.Extension.csproj new file mode 100644 index 00000000000..fae776deb73 --- /dev/null +++ b/src/FSharp.VisualStudio.Extension/FSharp.VisualStudio.Extension.csproj @@ -0,0 +1,41 @@ + + + net8.0-windows + enable + 12 + en-US + + + + + + + + + + + + + + + + + + + + + + false + + + + + diff --git a/src/FSharp.VisualStudio.Extension/FSharpExtensionSettings.cs b/src/FSharp.VisualStudio.Extension/FSharpExtensionSettings.cs new file mode 100644 index 00000000000..edc3e7adcff --- /dev/null +++ b/src/FSharp.VisualStudio.Extension/FSharpExtensionSettings.cs @@ -0,0 +1,55 @@ +using Microsoft.VisualStudio.Extensibility; +using Microsoft.VisualStudio.Extensibility.Settings; + +namespace FSharp.VisualStudio.Extension +{ +#pragma warning disable VSEXTPREVIEW_SETTINGS // The settings API is currently in preview and marked as experimental + + internal static class FSharpExtensionSettings + { + public const string OLD = "old"; + public const string LSP = "lsp"; + public const string BOTH = "both"; + public const string UNSET = "unset"; + + public static EnumSettingEntry[] ExtensionChoice { get; } = + [ + new(OLD, "%FSharpSettings.Old%"), + new(LSP, "%FSharpSettings.LSP%"), + new(BOTH, "%FSharpSettings.Both%"), + new(UNSET, "%FSharpSettings.Unset%"), + ]; + + + [VisualStudioContribution] + public static SettingCategory FSharpCategory { get; } = new("fsharp", "%F#%"); + + [VisualStudioContribution] + public static Setting.Enum GetDiagnosticsFrom { get; } = new( + "getDiagnosticsFrom", + "%FSharpSettings.GetDiagnosticsFrom%", + FSharpCategory, + ExtensionChoice, + defaultValue: UNSET) + { + Description = "%Which extension should be used to provide diagnostics%", + }; + + [VisualStudioContribution] + public static Setting.Enum GetSemanticHighlightingFrom { get; } = new( + "getSemanticHighlightingFrom", + "%FSharpSettings.GetSemanticHighlightingFrom%", + FSharpCategory, + ExtensionChoice, + defaultValue: UNSET) + { + Description = "%Which extension should be used to provide semantic highlighting%", + }; + + public static Setting[] AllStringSettings { get; } = + [ + GetDiagnosticsFrom, + GetSemanticHighlightingFrom, + ]; + } +} diff --git a/src/FSharp.VisualStudio.Extension/FSharpLanguageServerProvider.cs b/src/FSharp.VisualStudio.Extension/FSharpLanguageServerProvider.cs new file mode 100644 index 00000000000..c4f90a7ca1d --- /dev/null +++ b/src/FSharp.VisualStudio.Extension/FSharpLanguageServerProvider.cs @@ -0,0 +1,412 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace FSharp.VisualStudio.Extension; + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Pipelines; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FSharp.Compiler.CodeAnalysis.Workspace; +using FSharp.Compiler.Diagnostics; +using FSharp.Compiler.LanguageServer; +using FSharp.Compiler.LanguageServer.Common; + +using Microsoft.CommonLanguageServerProtocol.Framework; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.FSharp.Core; +using Microsoft.VisualStudio.Extensibility; +using Microsoft.VisualStudio.Extensibility.Editor; +using Microsoft.VisualStudio.Extensibility.LanguageServer; +using Microsoft.VisualStudio.Extensibility.Settings; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.ProjectSystem.Query; +using Microsoft.VisualStudio.RpcContracts.LanguageServerProvider; +using Nerdbank.Streams; + +/// +#pragma warning disable VSEXTPREVIEW_LSP // Type is for evaluation purposes only and is subject to change or removal in future updates. + +#pragma warning disable VSEXTPREVIEW_PROJECTQUERY_PROPERTIES_BUILDPROPERTIES // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + +internal static class Extensions +{ + public static List> Please(this IAsyncQueryable x) => x.QueryAsync(CancellationToken.None).ToBlockingEnumerable().ToList(); +} + + +internal class VsServerCapabilitiesOverride : IServerCapabilitiesOverride +{ + public ServerCapabilities OverrideServerCapabilities(FSharpLanguageServerConfig config, ServerCapabilities value, ClientCapabilities clientCapabilities) + { + var capabilities = new VSInternalServerCapabilities + { + TextDocumentSync = value.TextDocumentSync, + SupportsDiagnosticRequests = true, + ProjectContextProvider = true, + DiagnosticProvider = + config.EnabledFeatures.Diagnostics ? + + new() + { + SupportsMultipleContextsDiagnostics = true, + DiagnosticKinds = [ + // Support a specialized requests dedicated to task-list items. This way the client can ask just + // for these, independently of other diagnostics. They can also throttle themselves to not ask if + // the task list would not be visible. + //VSInternalDiagnosticKind.Task, + // Dedicated request for workspace-diagnostics only. We will only respond to these if FSA is on. + VSInternalDiagnosticKind.Syntax, + // Fine-grained diagnostics requests. Importantly, this separates out syntactic vs semantic + // requests, allowing the former to quickly reach the user without blocking on the latter. In a + // similar vein, compiler diagnostics are explicitly distinct from analyzer-diagnostics, allowing + // the former to appear as soon as possible as they are much more critical for the user and should + // not be delayed by a slow analyzer. + //new("Semantic"), + //new(PullDiagnosticCategories.DocumentAnalyzerSyntax), + //new(PullDiagnosticCategories.DocumentAnalyzerSemantic), + ] + } : null, + SemanticTokensOptions = config.EnabledFeatures.SemanticHighlighting ? new() + { + Legend = new() + { + TokenTypes = [.. SemanticTokenTypes.AllTypes], // XXX should be extended + TokenModifiers = [.. SemanticTokenModifiers.AllModifiers] + }, + Full = new SemanticTokensFullOptions() + { + Delta = false + }, + Range = false + } : null, + //, + //HoverProvider = new HoverOptions() + //{ + // WorkDoneProgress = true + //} + }; + return capabilities; + } +} + +internal class VsDiagnosticsHandler + : IRequestHandler, + IRequestHandler +{ + public bool MutatesSolutionState => false; + + [LanguageServerEndpoint(VSInternalMethods.DocumentPullDiagnosticName, LanguageServerConstants.DefaultLanguageName)] + public async Task HandleRequestAsync(VSInternalDiagnosticParams request, FSharpRequestContext context, CancellationToken cancellationToken) + { + var report = await context.Workspace.Query.GetDiagnosticsForFile(request!.TextDocument!.Uri).Please(cancellationToken); + + var vsReport = new VSInternalDiagnosticReport + { + ResultId = report.ResultId, + //Identifier = 1, + //Version = 1, + Diagnostics = [.. report.Diagnostics.Select(FSharpDiagnosticExtensions.ToLspDiagnostic)] + }; + + return [vsReport]; + } + + [LanguageServerEndpoint("textDocument/_vs_getProjectContexts", LanguageServerConstants.DefaultLanguageName)] + public Task HandleRequestAsync(VSGetProjectContextsParams request, FSharpRequestContext context, CancellationToken cancellationToken) + { + return Task.FromResult(new VSProjectContextList() + { + DefaultIndex = 0, + ProjectContexts = [ + //new() { + // Id = "potato", + // Label = "Potato", + // // PR for F# project kind: https://devdiv.visualstudio.com/DevDiv/_git/VSLanguageServerClient/pullrequest/529882 + // Kind = VSProjectKind.FSharp + //}, + //new () { + // Id = "potato2", + // Label = "Potato2", + // Kind = VSProjectKind.FSharp + //} + + ] + }); + } +} + + +internal class SolutionObserver : IObserver> +{ + public void OnCompleted() + { + + } + + public void OnError(Exception error) + { + } + + public void OnNext(IQueryResults value) + { + Trace.TraceInformation("Solution was updated"); + } + +} + +internal class ProjectObserver(FSharpWorkspace workspace) : IObserver> +{ + private readonly FSharpWorkspace workspace = workspace; + + internal void ProcessProject(IProjectSnapshot project) + { + project.Id.TryGetValue("ProjectPath", out var projectPath); + + List<(string, string)> projectInfos = []; + + if (projectPath != null && projectPath.ToLower().EndsWith(".fsproj")) + { + var configs = project.ActiveConfigurations.ToList(); + + foreach (var config in configs) + { + if (config != null) + { + // Extract bin output path for each active config + var data = config.OutputGroups; + + string? outputPath = null; + foreach (var group in data) + { + if (group.Name == "Built") + { + foreach (var output in group.Outputs) + { + if (output.FinalOutputPath != null && (output.FinalOutputPath.ToLower().EndsWith(".dll") || output.FinalOutputPath.ToLower().EndsWith(".exe"))) + { + outputPath = output.FinalOutputPath; + break; + } + } + if (outputPath != null) + { + break; + } + } + } + + foreach (var ruleResults in config.RuleResults) + { + // XXX Idk why `.Where` does not work with these IAsyncQueryable type + if (ruleResults?.RuleName == "CompilerCommandLineArgs") + { + // XXX Not sure why there would be more than one item for this rule result + // Taking first one, ignoring the rest + var args = ruleResults?.Items?.FirstOrDefault()?.Name; + if (args != null && outputPath != null) projectInfos.Add((outputPath, args)); + } + } + } + } + + foreach (var projectInfo in projectInfos) + { + workspace.Projects.AddOrUpdate(projectPath, projectInfo.Item1, projectInfo.Item2.Split(';')); + } + + //var graphPath = Path.Combine(Path.GetDirectoryName(projectPath) ?? ".", "..", "depGraph.md"); + + //workspace.projects.Debug_DumpGraphOnEveryChange = FSharpOption.Some(graphPath); + + //Trace.TraceInformation($"Auto-saving workspace graph to {graphPath}"); + + } + } + + public void OnNext(IQueryResults result) + { + foreach (var project in result) + { + this.ProcessProject(project); + } + } + + public void OnCompleted() + { + } + + public void OnError(Exception error) + { + } +} + + +[VisualStudioContribution] +internal class FSharpLanguageServerProvider : LanguageServerProvider +{ + /// + /// Gets the document type for FSharp code files. + /// + [VisualStudioContribution] + public static DocumentTypeConfiguration FSharpDocumentType => new("F#") + { + FileExtensions = [".fs", ".fsi", ".fsx"], + BaseDocumentType = LanguageServerBaseDocumentType, + }; + + /// + public override LanguageServerProviderConfiguration LanguageServerProviderConfiguration => new( + "%FSharpLspExtension.FSharpLanguageServerProvider.DisplayName%", + [Microsoft.VisualStudio.Extensibility.DocumentFilter.FromDocumentType(FSharpDocumentType)]); + + /// + public override async Task CreateServerConnectionAsync(CancellationToken cancellationToken) + { + var activitySourceName = "fsc"; + + FSharp.Compiler.LanguageServer.Activity.listenToSome(); + +#pragma warning disable VSEXTPREVIEW_SETTINGS // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + // Write default settings unless they're overridden. Otherwise users can't even find which settings exist. + + var settingsReadResult = await this.Extensibility.Settings().ReadEffectiveValuesAsync(FSharpExtensionSettings.AllStringSettings, cancellationToken); + + var settingValues = FSharpExtensionSettings.AllStringSettings.Select( + setting => (setting, settingsReadResult.ValueOrDefault(setting, defaultValue: FSharpExtensionSettings.UNSET))); + + foreach (var (setting, value) in settingValues.Where(x => x.Item2 == FSharpExtensionSettings.UNSET)) + { + await this.Extensibility.Settings().WriteAsync(batch => + batch.WriteSetting(setting, FSharpExtensionSettings.BOTH), "write default settings", cancellationToken); + } + + var enabled = new[] { FSharpExtensionSettings.LSP, FSharpExtensionSettings.BOTH }; + + var serverConfig = new FSharpLanguageServerConfig( + new FSharpLanguageServerFeatures( + diagnostics: enabled.Contains(settingsReadResult.ValueOrDefault(FSharpExtensionSettings.GetDiagnosticsFrom, defaultValue: FSharpExtensionSettings.BOTH)), + semanticHighlighting: enabled.Contains(settingsReadResult.ValueOrDefault(FSharpExtensionSettings.GetSemanticHighlightingFrom, defaultValue: FSharpExtensionSettings.BOTH)) + )); + + var disposeToEndSubscription = + this.Extensibility.Settings().SubscribeAsync( + [FSharpExtensionSettings.FSharpCategory], + cancellationToken, + changeHandler: result => + { + Trace.TraceInformation($"Settings update", result); + }); + +#pragma warning restore VSEXTPREVIEW_SETTINGS // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + + //const string vsMajorVersion = "17.0"; + + //var settings = OpenTelemetryExporterSettingsBuilder + // .CreateVSDefault(vsMajorVersion) + // .Build(); + + //try + //{ + // var tracerProvider = Sdk.CreateTracerProviderBuilder() + // .AddVisualStudioDefaultTraceExporter(settings) + // //.AddConsoleExporter() + // .AddOtlpExporter() + // .Build(); + //} + //catch (Exception e) + //{ + // Trace.TraceError($"Failed to create OpenTelemetry tracer provider: {e}"); + //} + + + var activitySource = new ActivitySource(activitySourceName); + var activity = activitySource.CreateActivity("CreateServerConnectionAsync", ActivityKind.Internal); + + if (activity != null) + { + activity.Start(); + } + else + { + Trace.TraceWarning("Failed to start OpenTelemetry activity, there are no listeners"); + } + + var ws = this.Extensibility.Workspaces(); + + var projectQuery = (IAsyncQueryable project) => project + .With(p => p.ActiveConfigurations + .With(c => c.ConfigurationDimensions.With(d => d.Name).With(d => d.Value)) + .With(c => c.Properties.With(p => p.Name).With(p => p.Value)) + .With(c => c.OutputGroups.With(g => g.Name).With(g => g.Outputs.With(o => o.Name).With(o => o.FinalOutputPath).With(o => o.RootRelativeURL))) + .With(c => c.RuleResultsByRuleName("CompilerCommandLineArgs") + .With(r => r.RuleName) + .With(r => r.Items))) + .With(p => p.ProjectReferences + .With(r => r.ReferencedProjectPath) + .With(r => r.CanonicalName) + .With(r => r.Id) + .With(r => r.Name) + .With(r => r.ProjectGuid) + .With(r => r.ReferencedProjectId) + .With(r => r.ReferenceType)); + + IQueryResults? result = await ws.QueryProjectsAsync(p => projectQuery(p).With(p => new { p.ActiveConfigurations, p.Id, p.Guid }), cancellationToken); + + var workspace = new FSharpWorkspace(); + + foreach (var project in result) + { + var observer = new ProjectObserver(workspace); + + await projectQuery(project.AsQueryable()).SubscribeAsync(observer, CancellationToken.None); + + // TODO: should we do this, or are we guaranteed it will get processed? + // observer.ProcessProject(project); + } + + var ((inputStream, outputStream), _server) = FSharpLanguageServer.Create(workspace, serverConfig, (serviceCollection) => + { + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + }); + + var solutions = await ws.QuerySolutionAsync( + solution => solution.With(solution => solution.FileName), + cancellationToken); + + var singleSolution = solutions.FirstOrDefault(); + + if (singleSolution != null) + { + var unsubscriber = await singleSolution + .AsQueryable() + .With(p => p.Projects.With(p => p.Files)) + .SubscribeAsync(new SolutionObserver(), CancellationToken.None); + } + + return new DuplexPipe( + PipeReader.Create(inputStream), + PipeWriter.Create(outputStream)); + } + + /// + public override Task OnServerInitializationResultAsync(ServerInitializationResult serverInitializationResult, LanguageServerInitializationFailureInfo? initializationFailureInfo, CancellationToken cancellationToken) + { + if (serverInitializationResult == ServerInitializationResult.Failed) + { + // Log telemetry for failure and disable the server from being activated again. + this.Enabled = false; + } + + return base.OnServerInitializationResultAsync(serverInitializationResult, initializationFailureInfo, cancellationToken); + } +} +#pragma warning restore VSEXTPREVIEW_LSP // Type is for evaluation purposes only and is subject to change or removal in future updates. diff --git a/src/Microsoft.CommonLanguageServerProtocol.Framework.Proxy/Microsoft.CommonLanguageServerProtocol.Framework.Proxy.csproj b/src/Microsoft.CommonLanguageServerProtocol.Framework.Proxy/Microsoft.CommonLanguageServerProtocol.Framework.Proxy.csproj new file mode 100644 index 00000000000..97ac5ccaebe --- /dev/null +++ b/src/Microsoft.CommonLanguageServerProtocol.Framework.Proxy/Microsoft.CommonLanguageServerProtocol.Framework.Proxy.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/tests/FSharp.Compiler.ComponentTests/CompilerService/FSharpWorkspace.fs b/tests/FSharp.Compiler.ComponentTests/CompilerService/FSharpWorkspace.fs index fbae320131d..6fd5062d35e 100644 --- a/tests/FSharp.Compiler.ComponentTests/CompilerService/FSharpWorkspace.fs +++ b/tests/FSharp.Compiler.ComponentTests/CompilerService/FSharpWorkspace.fs @@ -1,44 +1,61 @@ module CompilerService.FSharpWorkspace open System + open Xunit open FSharp.Compiler.CodeAnalysis.ProjectSnapshot -open TestFramework -open FSharp.Compiler.IO open FSharp.Compiler.CodeAnalysis.Workspace +open FSharp.Compiler.Diagnostics +open FSharp.Test.ProjectGeneration.WorkspaceHelpers +open OpenTelemetry +open OpenTelemetry.Resources +open OpenTelemetry.Trace +open OpenTelemetry.Exporter +open System.IO #nowarn "57" -type ProjectConfig with +// System.Diagnostics.DiagnosticSource seems to be missing in NET FW. Might investigate this later +#if !NETFRAMEWORK - static member Minimal(?name, ?outputPath, ?referencesOnDisk) = - let name = defaultArg name "test" - let projectFileName = $"{name}.fsproj" - let outputPath = defaultArg outputPath $"{name}.dll" - let referencesOnDisk = defaultArg referencesOnDisk [] - ProjectConfig(projectFileName, Some outputPath, referencesOnDisk, []) +//type FilteredJaegerExporter(predicate) = -let getReferencedSnapshot (projectIdentifier: FSharpProjectIdentifier) (projectSnapshot: FSharpProjectSnapshot) = - projectSnapshot.ReferencedProjects - |> Seq.pick (function - | FSharpReference(x, snapshot) when x = projectIdentifier.OutputFileName -> Some snapshot - | _ -> None) +// inherit SimpleActivityExportProcessor(new JaegerExporter(new JaegerExporterOptions())) -let sourceFileOnDisk (content: string) = - let path = getTemporaryFileName () + ".fs" - FileSystem.OpenFileForWriteShim(path).Write(content) - Uri(path) +// override _.OnEnd(activity: System.Diagnostics.Activity) = +// if predicate activity then +// base.OnEnd activity -let assertFileHasContent filePath expectedContent (projectSnapshot: FSharpProjectSnapshot) = - let fileSnapshot = - projectSnapshot.SourceFiles |> Seq.find (_.FileName >> (=) filePath) +/// Wrapper for FSharpWorkspace to use in tests. Provides OpenTelemetry tracing. +type TestingWorkspace(testName) as _this = + inherit FSharpWorkspace() + + let debugGraphPath = __SOURCE_DIRECTORY__ ++ $"{testName}.md" + + //let tracerProvider = + // Sdk + // .CreateTracerProviderBuilder() + // .AddSource("fsc") + // .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(serviceName="F#", serviceVersion = "1")) + // .AddProcessor(new FilteredJaegerExporter(_.DisplayName >> (<>) "DiagnosticsLogger.StackGuard.Guard")) + // .Build() + + let activity = Activity.start $"Test FSharpWorkspace {testName}" [] + + do _this.Projects.Debug_DumpGraphOnEveryChange <- Some debugGraphPath + + member _.DebugGraphPath = debugGraphPath - Assert.Equal(expectedContent, fileSnapshot.GetSource().Result.ToString()) + interface IDisposable with + member _.Dispose() = + use _ = activity in () + //tracerProvider.ForceFlush() |> ignore + //tracerProvider.Dispose() [] let ``Add project to workspace`` () = - let workspace = FSharpWorkspace() + use workspace = new TestingWorkspace("Add project to workspace") let projectPath = "test.fsproj" let outputPath = "test.dll" let compilerArgs = [| "test.fs" |] @@ -51,7 +68,7 @@ let ``Add project to workspace`` () = [] let ``Open file in workspace`` () = - let workspace = FSharpWorkspace() + use workspace = new TestingWorkspace("Open file in workspace") let fileUri = Uri("file:///test.fs") let content = "let x = 1" @@ -83,13 +100,13 @@ let ``Open file in workspace`` () = [] let ``Close file in workspace`` () = - let workspace = FSharpWorkspace() + use workspace = new TestingWorkspace("Close file in workspace") let contentOnDisk = "let x = 1" let fileOnDisk = sourceFileOnDisk contentOnDisk let _projectIdentifier = - workspace.Projects.AddOrUpdate(ProjectConfig.Minimal(), [ fileOnDisk.LocalPath ]) + workspace.Projects.AddOrUpdate(ProjectConfig.Empty(), [ fileOnDisk.LocalPath ]) workspace.Files.Open(fileOnDisk, contentOnDisk) @@ -118,12 +135,12 @@ let ``Close file in workspace`` () = [] let ``Change file in workspace`` () = - let workspace = FSharpWorkspace() + use workspace = new TestingWorkspace("Change file in workspace") let fileUri = Uri("file:///test.fs") let _projectIdentifier = - workspace.Projects.AddOrUpdate(ProjectConfig.Minimal(), [ fileUri.LocalPath ]) + workspace.Projects.AddOrUpdate(ProjectConfig.Empty(), [ fileUri.LocalPath ]) let initialContent = "let x = 2" @@ -144,7 +161,7 @@ let ``Change file in workspace`` () = [] let ``Add multiple projects with references`` () = - let workspace = FSharpWorkspace() + use workspace = new TestingWorkspace("Add multiple projects with references") let projectPath1 = "test1.fsproj" let outputPath1 = "test1.dll" let compilerArgs1 = [| "test1.fs" |] @@ -174,20 +191,20 @@ let ``Add multiple projects with references`` () = [] let ``Propagate changes to snapshots`` () = - let workspace = FSharpWorkspace() + use workspace = new TestingWorkspace("Propagate changes to snapshots") let file1 = sourceFileOnDisk "let x = 1" - let pid1 = workspace.Projects.AddOrUpdate(ProjectConfig.Minimal("p1"), [ file1.LocalPath ]) + let pid1 = workspace.Projects.AddOrUpdate(ProjectConfig.Empty("p1"), [ file1.LocalPath ]) let file2 = sourceFileOnDisk "let y = 2" let pid2 = - workspace.Projects.AddOrUpdate(ProjectConfig.Minimal("p2", referencesOnDisk = [ pid1.OutputFileName ]), [ file2.LocalPath ]) + workspace.Projects.AddOrUpdate(ProjectConfig.Empty("p2", referencesOnDisk = [ pid1.OutputFileName ]), [ file2.LocalPath ]) let file3 = sourceFileOnDisk "let z = 3" let pid3 = - workspace.Projects.AddOrUpdate(ProjectConfig.Minimal("p3", referencesOnDisk = [ pid2.OutputFileName ]), [ file3.LocalPath ]) + workspace.Projects.AddOrUpdate(ProjectConfig.Empty("p3", referencesOnDisk = [ pid2.OutputFileName ]), [ file3.LocalPath ]) let s3 = workspace.Query.GetProjectSnapshot(pid3).Value @@ -208,8 +225,8 @@ let ``Propagate changes to snapshots`` () = |> assertFileHasContent file1.LocalPath updatedContent [] -let ``Update project by adding a source file`` () = - let workspace = FSharpWorkspace() +let ``AddOrUpdate project by adding a source file`` () = + use workspace = new TestingWorkspace("Update project by adding a source file") let projectPath = "test.fsproj" let outputPath = "test.dll" let compilerArgs = [| "test.fs" |] @@ -222,9 +239,36 @@ let ``Update project by adding a source file`` () = Assert.Contains("test.fs", projectSnapshot.SourceFiles |> Seq.map (fun f -> f.FileName)) Assert.Contains(newSourceFile, projectSnapshot.SourceFiles |> Seq.map (fun f -> f.FileName)) +[] +let ``Update project by adding a source file`` () = + use workspace = new TestingWorkspace("Update project by adding a source file") + let projectPath = "test.fsproj" + let outputPath = "test.dll" + let compilerArgs = [| "test.fs" |] + let projectIdentifier = workspace.Projects.AddOrUpdate(projectPath, outputPath, compilerArgs) + let newSourceFile = "newTest.fs" + let newSourceFiles = [| "test.fs"; newSourceFile |] + workspace.Projects.Update(projectIdentifier, newSourceFiles) |> ignore + let projectSnapshot = workspace.Query.GetProjectSnapshot(projectIdentifier).Value + Assert.NotNull(projectSnapshot) + Assert.Contains("test.fs", projectSnapshot.SourceFiles |> Seq.map (fun f -> f.FileName)) + Assert.Contains(newSourceFile, projectSnapshot.SourceFiles |> Seq.map (fun f -> f.FileName)) + +[] +let ``Update project by removing a source file`` () = + use workspace = new TestingWorkspace("Update project by removing a source file") + let projectPath = "test.fsproj" + let outputPath = "test.dll" + let compilerArgs = [| "test.fs"; "newTest.fs" |] + let projectIdentifier = workspace.Projects.AddOrUpdate(projectPath, outputPath, compilerArgs) + let newCompilerArgs = [| "test.fs" |] + workspace.Projects.AddOrUpdate(projectPath, outputPath, newCompilerArgs) |> ignore + let files = workspace.Files.OfProject(projectIdentifier) |> Seq.toArray + Assert.Equal([| "test.fs" |], files) + [] let ``Update project by adding a reference`` () = - let workspace = FSharpWorkspace() + use workspace = new TestingWorkspace("Update project by adding a reference") let projectPath1 = "test1.fsproj" let outputPath1 = "test1.dll" let compilerArgs1 = [| "test1.fs" |] @@ -254,7 +298,7 @@ let ``Update project by adding a reference`` () = [] let ``Create references in existing projects`` () = - let workspace = FSharpWorkspace() + use workspace = new TestingWorkspace("Create references in existing projects") let projectPath1 = "test1.fsproj" let outputPath1 = "test1.dll" let compilerArgs1 = [| "test1.fs" |] @@ -296,6 +340,105 @@ let ``Create references in existing projects`` () = [] let ``Asking for an unknown project snapshot returns None`` () = - let workspace = FSharpWorkspace() + use workspace = new TestingWorkspace("Asking for an unknown project snapshot returns None") Assert.Equal(None, workspace.Query.GetProjectSnapshot(FSharpProjectIdentifier("hello", "world"))) + + +[] +let ``Works with signature files`` () = + task { + + use workspace = new TestingWorkspace("Works with signature files") + + let projectConfig = ProjectConfig.Create() + + let sourceFileUri = projectConfig.FileUri "test.fs" + + let source = "let x = 1" + + let projectIdentifier = workspace.Projects.AddOrUpdate(projectConfig, [ sourceFileUri ]) + + workspace.Files.Open(sourceFileUri, source) + + let! signatureUri, _signatureSource = workspace.AddSignatureFile(projectIdentifier, sourceFileUri, writeToDisk=false) + + let! diag = workspace.Query.GetDiagnosticsForFile(signatureUri) + + Assert.Equal(0, diag.Diagnostics.Length) + + workspace.Files.Edit(signatureUri, "module Test\n\nval x: potato") + + let! diag = workspace.Query.GetDiagnosticsForFile(signatureUri) + + Assert.Equal(1, diag.Diagnostics.Length) + Assert.Equal("The type 'potato' is not defined.", diag.Diagnostics[0].Message) + + workspace.Files.Edit(signatureUri, "module Test\n\nval y: int") + + let! diag = workspace.Query.GetDiagnosticsForFile(sourceFileUri) + + Assert.Equal(1, diag.Diagnostics.Length) + Assert.Equal("Module 'Test' requires a value 'y'", diag.Diagnostics[0].Message) + } + + +let reposDir = __SOURCE_DIRECTORY__ ++ ".." ++ ".." ++ ".." ++ ".." +let giraffeDir = reposDir ++ "Giraffe" ++ "src" ++ "Giraffe" |> Path.GetFullPath +let giraffeTestsDir = reposDir ++ "Giraffe" ++ "tests" ++ "Giraffe.Tests" |> Path.GetFullPath +let giraffeSampleDir = reposDir ++ "Giraffe" ++ "samples" ++ "EndpointRoutingApp" ++ "EndpointRoutingApp" |> Path.GetFullPath +let giraffeSignaturesDir = reposDir ++ "giraffe-signatures" ++ "src" ++ "Giraffe" |> Path.GetFullPath +let giraffeSignaturesTestsDir = reposDir ++ "giraffe-signatures" ++ "tests" ++ "Giraffe.Tests" |> Path.GetFullPath +let giraffeSignaturesSampleDir = reposDir ++ "giraffe-signatures" ++ "samples" ++ "EndpointRoutingApp" ++ "EndpointRoutingApp" |> Path.GetFullPath + +type GiraffeFactAttribute() = + inherit Xunit.FactAttribute() + do + if not (Directory.Exists giraffeDir) then + do base.Skip <- $"Giraffe not found ({giraffeDir}). You can get it here: https://github.com/giraffe-fsharp/Giraffe" + if not (Directory.Exists giraffeSignaturesDir) then + do base.Skip <- $"Giraffe (with signatures) not found ({giraffeSignaturesDir}). You can get it here: https://github.com/nojaf/Giraffe/tree/signatures" + + +[] +let ``Giraffe signature test`` () = + task { + use workspace = new TestingWorkspace("Giraffe signature test") + + let responseFileName = "compilerArgs.rsp" + + let _identifiers = + [ + giraffeSignaturesDir + giraffeSignaturesTestsDir + giraffeSignaturesSampleDir ] + |> Seq.map (fun dir -> + let projectName = Path.GetFileName dir + let dllName = $"{projectName}.dll" + let responseFile = dir ++ responseFileName + let outputFile = dir ++ "bin" ++ "Debug" ++ "net6.0" ++ dllName + let projectFile = dir ++ projectName + ".fsproj" + let compilerArgs = File.ReadAllLines responseFile + workspace.Projects.AddOrUpdate(projectFile, outputFile, compilerArgs) + ) + |> Seq.toList + + let _ = workspace.Files.OpenFromDisk(giraffeSignaturesSampleDir ++ "Program.fs") + + let! diag = workspace.Query.GetDiagnosticsForFile(Uri(giraffeSignaturesSampleDir ++ "Program.fs")) + Assert.Equal(0, diag.Diagnostics.Length) + + let middlewareFsiSource = workspace.Files.OpenFromDisk(giraffeSignaturesDir ++ "Middleware.fsi") + let middlewareFsiNewSource = middlewareFsiSource.Replace("static member AddGiraffe:", "static member AddGiraffe2:") + + let! diag = workspace.Query.GetDiagnosticsForFile(Uri(giraffeSignaturesDir ++ "Middleware.fsi")) + Assert.Equal(0, diag.Diagnostics.Length) + + workspace.Files.Edit(Uri(giraffeSignaturesDir ++ "Middleware.fsi"), middlewareFsiNewSource) + + let! diag = workspace.Query.GetDiagnosticsForFile(Uri(giraffeSignaturesSampleDir ++ "Program.fs")) + Assert.Equal(1, diag.Diagnostics.Length) + Assert.Equal("The type 'IServiceCollection' does not define the field, constructor or member 'AddGiraffe'.", diag.Diagnostics[0].Message) + } + +#endif \ No newline at end of file diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/CommonWorkflows.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/CommonWorkflows.fs index d8bd1f6e262..7ee7322a752 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/CommonWorkflows.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/CommonWorkflows.fs @@ -161,10 +161,10 @@ let ``We don't lose subsequent diagnostics when there's error in one file`` () = let project = { SyntheticProject.Create( { sourceFile "First" [] with - Source = """module AbstractBaseClass.File1 - + Source = """module AbstractBaseClass.File1 + let foo x = () - + a""" }, { sourceFile "Second" [] with Source = """module AbstractBaseClass.File2 @@ -178,7 +178,7 @@ let ``We don't lose subsequent diagnostics when there's error in one file`` () = abstract P: int""" }) with AutoAddModules = false SkipInitialCheck = true } - + project.Workflow { checkFile "First" (expectErrorCodes ["FS0039"]) checkFile "Second" (expectErrorCodes ["FS0054"; "FS0365"]) diff --git a/tests/FSharp.Compiler.LanguageServer.Tests/FSharp.Compiler.LanguageServer.Tests.fsproj b/tests/FSharp.Compiler.LanguageServer.Tests/FSharp.Compiler.LanguageServer.Tests.fsproj new file mode 100644 index 00000000000..046357aafe6 --- /dev/null +++ b/tests/FSharp.Compiler.LanguageServer.Tests/FSharp.Compiler.LanguageServer.Tests.fsproj @@ -0,0 +1,59 @@ + + + + $(FSharpNetCoreProductTargetFramework) + false + false + true + true + xunit + true + false + false + $(OtherFlags) --warnon:1182 + $(NoWarn);FS0988 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/FSharp.Compiler.LanguageServer.Tests/Program.fs b/tests/FSharp.Compiler.LanguageServer.Tests/Program.fs new file mode 100644 index 00000000000..80c6d842785 --- /dev/null +++ b/tests/FSharp.Compiler.LanguageServer.Tests/Program.fs @@ -0,0 +1,3 @@ +module Program = + [] + let main _ = 0 diff --git a/tests/FSharp.Compiler.LanguageServer.Tests/Protocol.fs b/tests/FSharp.Compiler.LanguageServer.Tests/Protocol.fs new file mode 100644 index 00000000000..9072fbfb460 --- /dev/null +++ b/tests/FSharp.Compiler.LanguageServer.Tests/Protocol.fs @@ -0,0 +1,193 @@ +module LanguageServer.Protocol + +open System +open Xunit + +open FSharp.Compiler.LanguageServer +open StreamJsonRpc +open System.IO +open System.Diagnostics + +open Microsoft.VisualStudio.LanguageServer.Protocol +open Nerdbank.Streams + +open FSharp.Test.ProjectGeneration.WorkspaceHelpers +open FSharp.Compiler.CodeAnalysis.Workspace +open FSharp.Compiler.CodeAnalysis.ProjectSnapshot + +#nowarn "57" + +type TestRpcClient(jsonRpc, rpcTrace, workspace, initializeResult: InitializeResult) = + + member val JsonRpc = jsonRpc + member val RpcTraceWriter = rpcTrace + member val Workspace = workspace + member val Capabilities = initializeResult.Capabilities + + member _.RpcTrace = rpcTrace.ToString() + +let initializeLanguageServer (workspace) = + + let workspace = defaultArg workspace (FSharpWorkspace()) + + // Create a StringWriter to capture the output + let rpcTrace = new StringWriter() + + let (inputStream, outputStream), server = FSharpLanguageServer.Create(workspace) + + let formatter = new JsonMessageFormatter() + + let messageHandler = + new HeaderDelimitedMessageHandler(inputStream, outputStream, formatter) + + let jsonRpc = new JsonRpc(messageHandler) + + // Create a new TraceListener with the StringWriter + let listener = new TextWriterTraceListener(rpcTrace) + + // Add the listener to the JsonRpc TraceSource + server.JsonRpc.TraceSource.Listeners.Add(listener) |> ignore + + // Set the TraceLevel to Information to get all informational, warning and error messages + server.JsonRpc.TraceSource.Switch.Level <- SourceLevels.All + + let initializeParams = + InitializeParams( + ProcessId = System.Diagnostics.Process.GetCurrentProcess().Id, + RootUri = Uri("file:///c:/temp"), + InitializationOptions = None + ) + + jsonRpc.StartListening() + + task { + let! response = jsonRpc.InvokeAsync("initialize", initializeParams) + return TestRpcClient(jsonRpc, rpcTrace, workspace, response) + } + +[] +let ``The server can process the initialization message`` () = + task { + + let! client = initializeLanguageServer None + Assert.NotNull(client.Capabilities) + + } + +/// Initialize workspace, open a document, get diagnostics, edit the document, get diagnostics, close the document +[] +let ``Basic server workflow`` () = + task { + + let! client = initializeLanguageServer None + + let workspace = client.Workspace + + let contentOnDisk = "let x = 1" + let fileOnDisk = sourceFileOnDisk contentOnDisk + + let _projectIdentifier = + workspace.Projects.AddOrUpdate(ProjectConfig.Create(), [ fileOnDisk.LocalPath ]) + + do! + client.JsonRpc.NotifyAsync( + Methods.TextDocumentDidOpenName, + DidOpenTextDocumentParams( + TextDocument = TextDocumentItem(Uri = fileOnDisk, LanguageId = "F#", Version = 1, Text = contentOnDisk) + ) + ) + + let! diagnosticsResponse = + client.JsonRpc.InvokeAsync>( + Methods.TextDocumentDiagnosticName, + DocumentDiagnosticParams(TextDocument = TextDocumentIdentifier(Uri = fileOnDisk)) + ) + + Assert.Equal( + 0, + (diagnosticsResponse.First.RelatedDocuments + |> Seq.head + |> _.Value.First.Items.Length) + ) + + let contentEdit = $"{contentOnDisk}\nx <- 2" + + do! + client.JsonRpc.NotifyAsync( + Methods.TextDocumentDidChangeName, + DidChangeTextDocumentParams( + TextDocument = VersionedTextDocumentIdentifier(Uri = fileOnDisk, Version = 2), + ContentChanges = [| TextDocumentContentChangeEvent(Text = contentEdit) |] + ) + ) + + let! diagnosticsResponse = + client.JsonRpc.InvokeAsync>( + Methods.TextDocumentDiagnosticName, + DocumentDiagnosticParams(TextDocument = TextDocumentIdentifier(Uri = fileOnDisk)) + ) + + let diagnostics = + diagnosticsResponse.First.RelatedDocuments |> Seq.head |> _.Value.First.Items + + Assert.Equal(1, diagnostics.Length) + Assert.Contains("This value is not mutable", diagnostics[0].Message) + + do! + client.JsonRpc.NotifyAsync( + Methods.TextDocumentDidCloseName, + DidCloseTextDocumentParams(TextDocument = TextDocumentIdentifier(Uri = fileOnDisk)) + ) + + let! diagnosticsResponse = + client.JsonRpc.InvokeAsync>( + Methods.TextDocumentDiagnosticName, + DocumentDiagnosticParams(TextDocument = TextDocumentIdentifier(Uri = fileOnDisk)) + ) + + // We didn't save the file, so it should be again read from disk and have no diagnostics + Assert.Equal( + 0, + (diagnosticsResponse.First.RelatedDocuments + |> Seq.head + |> _.Value.First.Items.Length) + ) + } + +[] +let ``Full semantic tokens`` () = + + task { + let! client = initializeLanguageServer None + let workspace = client.Workspace + let contentOnDisk = "let x = 1" + let fileOnDisk = sourceFileOnDisk contentOnDisk + let _projectIdentifier = + workspace.Projects.AddOrUpdate(ProjectConfig.Create(), [ fileOnDisk.LocalPath ]) + do! + client.JsonRpc.NotifyAsync( + Methods.TextDocumentDidOpenName, + DidOpenTextDocumentParams( + TextDocument = TextDocumentItem(Uri = fileOnDisk, LanguageId = "F#", Version = 1, Text = contentOnDisk) + ) + ) + let! semanticTokensResponse = + client.JsonRpc.InvokeAsync( + Methods.TextDocumentSemanticTokensFullName, + SemanticTokensParams(TextDocument = TextDocumentIdentifier(Uri = fileOnDisk)) + ) + + let expected = [| 0; 0; 0; 1; 0; 0; 0; 3; 15; 0; 0; 4; 1; 17; 0; 0; 4; 1; 19; 0 |] + + Assert.Equal(expected, semanticTokensResponse.Data) + } + +[] +let ``Shutdown and exit`` () = + task { + let! client = initializeLanguageServer None + + let! _respone = client.JsonRpc.InvokeAsync<_>(Methods.ShutdownName) + + do! client.JsonRpc.NotifyAsync(Methods.ExitName) + } diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.debug.bsl b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.debug.bsl index 6fccd622eca..4f1bda7afd1 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.debug.bsl +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.debug.bsl @@ -2420,6 +2420,8 @@ FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: Boolean get_ FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: FSharpProjectIdentifier Identifier FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: FSharpProjectIdentifier get_Identifier() FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: FSharpProjectSnapshot Create(System.String, Microsoft.FSharp.Core.FSharpOption`1[System.String], Microsoft.FSharp.Core.FSharpOption`1[System.String], Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot], Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.CodeAnalysis.ProjectSnapshot+ReferenceOnDisk], Microsoft.FSharp.Collections.FSharpList`1[System.String], Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpReferencedProjectSnapshot], Boolean, Boolean, System.DateTime, Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.CodeAnalysis.FSharpUnresolvedReferencesSet], Microsoft.FSharp.Collections.FSharpList`1[System.Tuple`3[FSharp.Compiler.Text.Range,System.String,System.String]], Microsoft.FSharp.Core.FSharpOption`1[System.Int64]) +FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: FSharpProjectSnapshot FromCommandLineArgs(System.String[], System.String, System.String) +FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: FSharpProjectSnapshot FromResponseFile(System.IO.FileInfo, System.String) FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: FSharpProjectSnapshot Replace(Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot]) FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot] SourceFiles FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot] get_SourceFiles() @@ -2442,6 +2444,8 @@ FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: Microsoft.FS FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: Microsoft.FSharp.Core.FSharpOption`1[System.String] ProjectId FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: Microsoft.FSharp.Core.FSharpOption`1[System.String] get_OutputFileName() FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: Microsoft.FSharp.Core.FSharpOption`1[System.String] get_ProjectId() +FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: ProjectConfig ProjectConfig +FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: ProjectConfig get_ProjectConfig() FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: System.DateTime LoadTime FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: System.DateTime get_LoadTime() FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: System.String Label @@ -2635,19 +2639,23 @@ FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpDiagnosticRepo FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpDiagnosticReport: System.String ResultId FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpDiagnosticReport: System.String get_ResultId() FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpWorkspaceQuery: Microsoft.FSharp.Control.FSharpAsync`1[FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpDiagnosticReport] GetDiagnosticsForFile(System.Uri) +FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpWorkspaceQuery: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults]] GetCheckResultsForFile(System.Uri) FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpWorkspaceQuery: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.EditorServices.SemanticClassificationView]] GetSemanticClassification(System.Uri) +FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpWorkspaceQuery: Microsoft.FSharp.Control.FSharpAsync`1[System.Tuple`2[Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.CodeAnalysis.FSharpParseFileResults],Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults]]] GetParseAndCheckResultsForFile(System.Uri) FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpWorkspaceQuery: Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot] GetProjectSnapshot(FSharpProjectIdentifier) FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpWorkspaceQuery: Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot] GetProjectSnapshotForFile(System.Uri) FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpWorkspaceQuery: System.Threading.Tasks.Task`1[Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Text.ISourceTextNew]] GetSource(System.Uri) FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery: FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpDiagnosticReport FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery: FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpWorkspaceQuery -FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceFiles: Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`2[System.Uri,System.String],Microsoft.FSharp.Core.Unit] Open -FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceFiles: Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`2[System.Uri,System.String],Microsoft.FSharp.Core.Unit] get_Open() +FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceFiles: System.Collections.Generic.IEnumerable`1[System.String] OfProject(FSharpProjectIdentifier) FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceFiles: Void Close(System.Uri) FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceFiles: Void Edit(System.Uri, System.String) +FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceFiles: Void Open(System.Uri, System.String) FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceProjects: FSharpProjectIdentifier AddOrUpdate(ProjectConfig, System.Collections.Generic.IEnumerable`1[System.String]) +FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceProjects: FSharpProjectIdentifier AddOrUpdate(ProjectConfig, System.Collections.Generic.IEnumerable`1[System.Uri]) FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceProjects: FSharpProjectIdentifier AddOrUpdate(System.String, System.String, System.Collections.Generic.IEnumerable`1[System.String]) FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceProjects: FSharpProjectIdentifier AddOrUpdate(System.String, System.String, System.Collections.Generic.IEnumerable`1[System.String], System.Collections.Generic.IEnumerable`1[System.String], Microsoft.FSharp.Collections.FSharpList`1[System.String]) +FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceProjects: Void Update(FSharpProjectIdentifier, System.Collections.Generic.IEnumerable`1[System.String]) FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState: FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceFiles FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState: FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceProjects FSharp.Compiler.CompilerEnvironment: Boolean IsCheckerSupportedSubcategory(System.String) diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.release.bsl b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.release.bsl index 566ff039e93..703e5029e32 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.release.bsl +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.release.bsl @@ -2420,6 +2420,8 @@ FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: Boolean get_ FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: FSharpProjectIdentifier Identifier FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: FSharpProjectIdentifier get_Identifier() FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: FSharpProjectSnapshot Create(System.String, Microsoft.FSharp.Core.FSharpOption`1[System.String], Microsoft.FSharp.Core.FSharpOption`1[System.String], Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot], Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.CodeAnalysis.ProjectSnapshot+ReferenceOnDisk], Microsoft.FSharp.Collections.FSharpList`1[System.String], Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpReferencedProjectSnapshot], Boolean, Boolean, System.DateTime, Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.CodeAnalysis.FSharpUnresolvedReferencesSet], Microsoft.FSharp.Collections.FSharpList`1[System.Tuple`3[FSharp.Compiler.Text.Range,System.String,System.String]], Microsoft.FSharp.Core.FSharpOption`1[System.Int64]) +FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: FSharpProjectSnapshot FromCommandLineArgs(System.String[], System.String, System.String) +FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: FSharpProjectSnapshot FromResponseFile(System.IO.FileInfo, System.String) FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: FSharpProjectSnapshot Replace(Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot]) FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot] SourceFiles FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: Microsoft.FSharp.Collections.FSharpList`1[FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot] get_SourceFiles() @@ -2442,6 +2444,8 @@ FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: Microsoft.FS FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: Microsoft.FSharp.Core.FSharpOption`1[System.String] ProjectId FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: Microsoft.FSharp.Core.FSharpOption`1[System.String] get_OutputFileName() FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: Microsoft.FSharp.Core.FSharpOption`1[System.String] get_ProjectId() +FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: ProjectConfig ProjectConfig +FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: ProjectConfig get_ProjectConfig() FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: System.DateTime LoadTime FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: System.DateTime get_LoadTime() FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot: System.String Label @@ -2635,19 +2639,23 @@ FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpDiagnosticRepo FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpDiagnosticReport: System.String ResultId FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpDiagnosticReport: System.String get_ResultId() FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpWorkspaceQuery: Microsoft.FSharp.Control.FSharpAsync`1[FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpDiagnosticReport] GetDiagnosticsForFile(System.Uri) +FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpWorkspaceQuery: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults]] GetCheckResultsForFile(System.Uri) FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpWorkspaceQuery: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.EditorServices.SemanticClassificationView]] GetSemanticClassification(System.Uri) +FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpWorkspaceQuery: Microsoft.FSharp.Control.FSharpAsync`1[System.Tuple`2[Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.CodeAnalysis.FSharpParseFileResults],Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.CodeAnalysis.FSharpCheckFileResults]]] GetParseAndCheckResultsForFile(System.Uri) FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpWorkspaceQuery: Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot] GetProjectSnapshot(FSharpProjectIdentifier) FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpWorkspaceQuery: Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpProjectSnapshot] GetProjectSnapshotForFile(System.Uri) FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpWorkspaceQuery: System.Threading.Tasks.Task`1[Microsoft.FSharp.Core.FSharpOption`1[FSharp.Compiler.Text.ISourceTextNew]] GetSource(System.Uri) FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery: FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpDiagnosticReport FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery: FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery+FSharpWorkspaceQuery -FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceFiles: Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`2[System.Uri,System.String],Microsoft.FSharp.Core.Unit] Open -FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceFiles: Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`2[System.Uri,System.String],Microsoft.FSharp.Core.Unit] get_Open() +FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceFiles: System.Collections.Generic.IEnumerable`1[System.String] OfProject(FSharpProjectIdentifier) FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceFiles: Void Close(System.Uri) FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceFiles: Void Edit(System.Uri, System.String) +FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceFiles: Void Open(System.Uri, System.String) FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceProjects: FSharpProjectIdentifier AddOrUpdate(ProjectConfig, System.Collections.Generic.IEnumerable`1[System.String]) +FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceProjects: FSharpProjectIdentifier AddOrUpdate(ProjectConfig, System.Collections.Generic.IEnumerable`1[System.Uri]) FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceProjects: FSharpProjectIdentifier AddOrUpdate(System.String, System.String, System.Collections.Generic.IEnumerable`1[System.String]) FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceProjects: FSharpProjectIdentifier AddOrUpdate(System.String, System.String, System.Collections.Generic.IEnumerable`1[System.String], System.Collections.Generic.IEnumerable`1[System.String], Microsoft.FSharp.Collections.FSharpList`1[System.String]) +FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceProjects: Void Update(FSharpProjectIdentifier, System.Collections.Generic.IEnumerable`1[System.String]) FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState: FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceFiles FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState: FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState+FSharpWorkspaceProjects FSharp.Compiler.CompilerEnvironment: Boolean IsCheckerSupportedSubcategory(System.String) diff --git a/tests/FSharp.Test.Utilities/ProjectGeneration.fs b/tests/FSharp.Test.Utilities/ProjectGeneration.fs index 92256c2b09b..fe10fb4f0fc 100644 --- a/tests/FSharp.Test.Utilities/ProjectGeneration.fs +++ b/tests/FSharp.Test.Utilities/ProjectGeneration.fs @@ -24,6 +24,7 @@ open System.Text.RegularExpressions open System.Threading.Tasks open System.Xml +open Internal.Utilities.Library.Extras open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.CodeAnalysis.ProjectSnapshot open FSharp.Compiler.Diagnostics @@ -36,6 +37,11 @@ open FSharp.Test.Utilities open OpenTelemetry open OpenTelemetry.Resources open OpenTelemetry.Trace +open TestFramework +open FSharp.Compiler.IO +open FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceState +open FSharp.Compiler.CodeAnalysis.Workspace.FSharpWorkspaceQuery +open FSharp.Compiler.CodeAnalysis.Workspace #nowarn "57" // Experimental feature use @@ -95,7 +101,7 @@ module ReferenceHelpers = { Name = matches.Groups.[1].Value Version = version - Path = DirectoryInfo(Path.Combine(matches.Groups[3].Value, version)) }) + Path = DirectoryInfo(Path.Combine(matches.Groups[3].Value, version)) }) |> Seq.toList) let getFrameworkReference (reference: Reference) = @@ -264,7 +270,7 @@ type SyntheticProject = static member Create(name: string, [] sourceFiles: SyntheticSourceFile[]) = { SyntheticProject.Create(name) with SourceFiles = sourceFiles |> List.ofArray } - + static member CreateForScript(scriptFile: SyntheticSourceFile) = { SyntheticProject.Create() with SourceFiles = [scriptFile]; UseScriptResolutionRules = true } @@ -485,14 +491,14 @@ let private writeFile (p: SyntheticProject) (f: SyntheticSourceFile) = let mkSyntheticProjectForResponseFile (responseFile: FileInfo) : SyntheticProject = if not responseFile.Exists then failwith $"%s{responseFile.FullName} does not exist" - + let compilerArgs = File.ReadAllLines responseFile.FullName let fsharpFileExtensions = set [| ".fs" ; ".fsi" ; ".fsx" |] let isFSharpFile (file : string) = Set.exists (fun (ext : string) -> file.EndsWith (ext, StringComparison.Ordinal)) fsharpFileExtensions - + let fsharpFiles = compilerArgs |> Array.choose (fun (line : string) -> @@ -517,7 +523,7 @@ let mkSyntheticProjectForResponseFile (responseFile: FileInfo) : SyntheticProjec implementationFiles |> List.map (fun implPath -> let id = - let fileNameWithoutExtension = Path.GetFileNameWithoutExtension implPath + let fileNameWithoutExtension = Path.GetFileNameWithoutExtension implPath let directoryOfFile = FileInfo(implPath).DirectoryName let relativeUri = Uri(responseFile.FullName).MakeRelativeUri(Uri(directoryOfFile)) let relativeFolderPath = Uri.UnescapeDataString(relativeUri.ToString()).Replace('/', Path.DirectorySeparatorChar) @@ -536,10 +542,10 @@ let mkSyntheticProjectForResponseFile (responseFile: FileInfo) : SyntheticProjec Source = File.ReadAllText implPath ExtraSource = "" EntryPoint = false - IsPhysicalFile = true + IsPhysicalFile = true } ) - + let otherOptions = compilerArgs |> Array.filter (fun line -> not (isFSharpFile line)) @@ -770,13 +776,13 @@ module ProjectOperations = |> Seq.toArray Assert.Equal<(string * int * int * int)[]>(expected |> Seq.sort |> Seq.toArray, actual) - + let expectNone x = if Option.isSome x then failwith "expected None, but was Some" - + let expectSome x = if Option.isNone x then failwith "expected Some, but was None" - + let rec saveProject (p: SyntheticProject) generateSignatureFiles checker = async { Directory.CreateDirectory(p.ProjectDir) |> ignore @@ -809,19 +815,19 @@ module ProjectOperations = let rec absorbAutoGeneratedSignatures checker (p: SyntheticProject) = async { do! saveProject p true checker - let files = [ + let files = [ for file in p.SourceFiles do if file.SignatureFile = AutoGenerated then let text = file |> getSignatureFilePath p |> File.ReadAllText { file with SignatureFile = Custom text } - else file + else file ] - let! projects = - p.DependsOn + let! projects = + p.DependsOn |> Seq.map (absorbAutoGeneratedSignatures checker) |> Async.Sequential - return - { p with + return + { p with SourceFiles = files AutoAddModules = false DependsOn = projects |> Array.toList } @@ -937,7 +943,7 @@ let SaveAndCheckProject project checker isExistingProject = Cursor = None } } -type MoveFileDirection = Up | Down +type MoveFileDirection = Up | Down type ProjectWorkflowBuilder ( @@ -1349,7 +1355,7 @@ type ProjectWorkflowBuilder if ex.IsSome then raise ex.Value return ctx } - + [] member this.TryGetRecentCheckResults(workflow: Async, fileId: string, expected) = async { @@ -1360,15 +1366,15 @@ type ProjectWorkflowBuilder let! snapshot = FSharpProjectSnapshot.FromOptions(options, getFileSnapshot ctx.Project) let r = checker.TryGetRecentCheckResultsForFile(fileName, snapshot) expected r - + match r with | Some(parseFileResults, checkFileResults) -> - let signature = getSignature(parseFileResults, FSharpCheckFileAnswer.Succeeded(checkFileResults)) + let signature = getSignature(parseFileResults, FSharpCheckFileAnswer.Succeeded(checkFileResults)) match ctx.Signatures.TryFind(fileId) with | Some priorSignature -> Assert.Equal(priorSignature, signature) | None -> () | None -> () - + return ctx } @@ -1411,7 +1417,7 @@ type SyntheticProject with projectDir ++ node.Attributes["Include"].InnerText ] |> List.partition (fun path -> path.EndsWith ".fsi") let signatureFiles = set signatureFiles - + let parseReferences refType = [ for node in fsproj.DocumentElement.SelectNodes($"//{refType}") do { Name = node.Attributes["Include"].InnerText @@ -1437,3 +1443,101 @@ type SyntheticProject with OtherOptions = [ for w in nowarns do $"--nowarn:{w}" ] } + + +module WorkspaceHelpers = + + let createTestProjectDirName () = + let projectDirectoryName = $"FSharp-Test-Project-{Guid.NewGuid().ToString()[..8]}" + Path.GetTempPath() ++ projectDirectoryName + + type ProjectConfig with + + /// Creates an empty project configuration with optional parameters for name, output path, and references on disk. + static member Empty(?name, ?outputPath, ?referencesOnDisk) = + let directory = createTestProjectDirName() + let name = defaultArg name "test" + let projectFileName = directory ++ $"{name}.fsproj" + let outputPath = defaultArg outputPath (directory ++ $"{name}.dll") + let referencesOnDisk = defaultArg referencesOnDisk [] + ProjectConfig(projectFileName, Some outputPath, referencesOnDisk, []) + + static member Create(?name) = + let name = defaultArg name "test" + let fullPath = createTestProjectDirName() ++ $"{name}.fsproj" + let snapshot, _ = + CompilerAssertHelpers.checker.GetProjectSnapshotFromScript(fullPath, SourceTextNew.ofString "", assumeDotNetFramework = false) + |> Async.RunSynchronously + snapshot.ProjectConfig + + /// Returns URI of a file in the project directory. + member this.FileUri(fileName) = + this.ProjectDirectory ++ fileName |> Uri + + type FSharpWorkspaceQuery with + + member this.GetSignature(sourceFile: Uri) = + use _ = Activity.start "FSharpWorkspace.GetSignature" [ Activity.Tags.fileName, sourceFile.LocalPath ] + this.GetCheckResultsForFile(sourceFile) + |> Async.map (Option.bind(_.GenerateSignature())) + + type FSharpWorkspaceProjects with + + member projects.AddFileBefore(projectIdentifier, newFile: Uri, addBefore: Uri) = + use _ = Activity.start "FSharpWorkspace.AddFileBefore" [ Activity.Tags.project, projectIdentifier.ToString(); "newFile", newFile.LocalPath; "addBefore", addBefore.LocalPath ] + + let existingFiles = projects.files.OfProject projectIdentifier + let newFiles = seq { + for file in existingFiles do + if file = addBefore.LocalPath then + newFile.LocalPath + file + } + projects.Update(projectIdentifier, newFiles) + + type FSharpWorkspaceFiles with + + /// Opens the file from a given path in the Workspace using content read from disk. + member this.OpenFromDisk(path) = + let content = FileSystem.OpenFileForReadShim(path).ReadAllText() + this.Open(Uri path, content) + content + + type FSharpWorkspace with + + member this.AddSignatureFile(projectIdentifier, sourceFile: Uri, ?writeToDisk) = + use _ = Activity.start "FSharpWorkspace.AddSignatureFile" [ Activity.Tags.project, projectIdentifier.ToString(); "sourceFile", sourceFile.LocalPath ] + + let writeToDisk = defaultArg writeToDisk true + async { + match! this.Query.GetSignature sourceFile with + | None -> return failwith $"Couldn't get signature for {sourceFile}" + | Some signature -> + let signatureFileUri = Uri $"{sourceFile.LocalPath}i" + if writeToDisk then + FileSystem.OpenFileForWriteShim(signatureFileUri.LocalPath).Write(signature) + else + this.Files.Open(signatureFileUri, signature.ToString()) + this.Projects.AddFileBefore(projectIdentifier, newFile=signatureFileUri, addBefore=sourceFile) + return signatureFileUri, signature + } + + /// Retrieves the referenced project snapshot from the given project snapshot based on the project identifier. + let getReferencedSnapshot (projectIdentifier: FSharpProjectIdentifier) (projectSnapshot: FSharpProjectSnapshot) = + projectSnapshot.ReferencedProjects + |> Seq.pick (function + | FSharpReference(x, snapshot) when x = projectIdentifier.OutputFileName -> Some snapshot + | _ -> None) + + /// Creates a temporary source file on disk with the given content and returns its URI. + let sourceFileOnDisk (content: string) = + let path = getTemporaryFileName () + ".fs" + FileSystem.OpenFileForWriteShim(path).Write(content) + Uri(path) + + /// Asserts that the file at the given path within the project snapshot has the expected content. + let assertFileHasContent filePath expectedContent (projectSnapshot: FSharpProjectSnapshot) = + let fileSnapshot = + projectSnapshot.SourceFiles |> Seq.find (_.FileName >> (=) filePath) + + Assert.Equal(expectedContent, fileSnapshot.GetSource().Result.ToString()) diff --git a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_net9.0.bsl b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_net9.0.bsl index da08a645140..f1a7a58efc2 100644 --- a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_net9.0.bsl +++ b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_net9.0.bsl @@ -21,7 +21,7 @@ [IL]: Error [StackUnexpected]: : FSharp.Compiler.CodeAnalysis.Hosted.CompilerHelpers::fscCompile([FSharp.Compiler.Service]FSharp.Compiler.CodeAnalysis.LegacyReferenceResolver, string, string[])][offset 0x00000082][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.CodeAnalysis.Hosted.CompilerHelpers::fscCompile([FSharp.Compiler.Service]FSharp.Compiler.CodeAnalysis.LegacyReferenceResolver, string, string[])][offset 0x0000008B][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+MagicAssemblyResolution::ResolveAssemblyCore([FSharp.Compiler.Service]Internal.Utilities.Library.CompilationThreadToken, [FSharp.Compiler.Service]FSharp.Compiler.Text.Range, [FSharp.Compiler.Service]FSharp.Compiler.CompilerConfig+TcConfigBuilder, [FSharp.Compiler.Service]FSharp.Compiler.CompilerImports+TcImports, [FSharp.Compiler.Service]FSharp.Compiler.Interactive.Shell+FsiDynamicCompiler, [FSharp.Compiler.Service]FSharp.Compiler.Interactive.Shell+FsiConsoleOutput, string)][offset 0x00000015][found Char] Unexpected type on the stack. -[IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+clo@3494-811::Invoke([S.P.CoreLib]System.Tuple`3)][offset 0x000001E5][found Char] Unexpected type on the stack. +[IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+clo@3494-813::Invoke([S.P.CoreLib]System.Tuple`3)][offset 0x000001E5][found Char] Unexpected type on the stack. [IL]: Error [UnmanagedPointer]: : FSharp.Compiler.Interactive.Shell+Utilities+pointerToNativeInt@106::Invoke(object)][offset 0x00000007] Unmanaged pointers are not a verifiable type. [IL]: Error [StackUnexpected]: : .$FSharpCheckerResults+dataTipOfReferences@2225::Invoke([FSharp.Core]Microsoft.FSharp.Core.Unit)][offset 0x00000084][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@924-515::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000032][found Char] Unexpected type on the stack. diff --git a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl index 92de58c4615..092da45d2e9 100644 --- a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl +++ b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Debug_netstandard2.0.bsl @@ -28,7 +28,7 @@ [IL]: Error [StackUnexpected]: : FSharp.Compiler.CodeAnalysis.Hosted.CompilerHelpers::fscCompile([FSharp.Compiler.Service]FSharp.Compiler.CodeAnalysis.LegacyReferenceResolver, string, string[])][offset 0x0000008B][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+FsiStdinSyphon::GetLine(string, int32)][offset 0x00000039][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+MagicAssemblyResolution::ResolveAssemblyCore([FSharp.Compiler.Service]Internal.Utilities.Library.CompilationThreadToken, [FSharp.Compiler.Service]FSharp.Compiler.Text.Range, [FSharp.Compiler.Service]FSharp.Compiler.CompilerConfig+TcConfigBuilder, [FSharp.Compiler.Service]FSharp.Compiler.CompilerImports+TcImports, [FSharp.Compiler.Service]FSharp.Compiler.Interactive.Shell+FsiDynamicCompiler, [FSharp.Compiler.Service]FSharp.Compiler.Interactive.Shell+FsiConsoleOutput, string)][offset 0x00000015][found Char] Unexpected type on the stack. -[IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+clo@3494-811::Invoke([S.P.CoreLib]System.Tuple`3)][offset 0x000001E5][found Char] Unexpected type on the stack. +[IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+clo@3494-813::Invoke([S.P.CoreLib]System.Tuple`3)][offset 0x000001E5][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+FsiInteractionProcessor::CompletionsForPartialLID([FSharp.Compiler.Service]FSharp.Compiler.Interactive.Shell+FsiDynamicCompilerState, string)][offset 0x0000001B][found Char] Unexpected type on the stack. [IL]: Error [UnmanagedPointer]: : FSharp.Compiler.Interactive.Shell+Utilities+pointerToNativeInt@106::Invoke(object)][offset 0x00000007] Unmanaged pointers are not a verifiable type. [IL]: Error [StackUnexpected]: : .$FSharpCheckerResults+dataTipOfReferences@2225::Invoke([FSharp.Core]Microsoft.FSharp.Core.Unit)][offset 0x00000084][found Char] Unexpected type on the stack. @@ -80,7 +80,7 @@ [IL]: Error [StackUnexpected]: : FSharp.Compiler.AbstractIL.IL::parseILVersion(string)][offset 0x0000000B][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.AbstractIL.IL::parseILVersion(string)][offset 0x00000021][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.AbstractIL.IL+parseNamed@5311::Invoke([FSharp.Core]Microsoft.FSharp.Collections.FSharpList`1>, int32, int32)][offset 0x00000087][found Char] Unexpected type on the stack. -[IL]: Error [StackUnexpected]: : Internal.Utilities.Collections.Utils::shortPath(string)][offset 0x00000015][found Char] Unexpected type on the stack. +[IL]: Error [StackUnexpected]: : Internal.Utilities.Collections.Utils::shortPath(string)][offset 0x00000048][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : Internal.Utilities.FSharpEnvironment+probePathForDotnetHost@317::Invoke([FSharp.Core]Microsoft.FSharp.Core.Unit)][offset 0x00000028][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.CodeAnalysis.SimulatedMSBuildReferenceResolver+Pipe #6 input at line 68@68::FSharp.Compiler.CodeAnalysis.ILegacyReferenceResolver.Resolve([FSharp.Compiler.Service]FSharp.Compiler.CodeAnalysis.LegacyResolutionEnvironment, [S.P.CoreLib]System.Tuple`2[], string, [FSharp.Core]Microsoft.FSharp.Collections.FSharpList`1, string, string, [FSharp.Core]Microsoft.FSharp.Collections.FSharpList`1, string, [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2, [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2>>)][offset 0x0000034D][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : .$FSharp.Compiler.DiagnosticsLogger::.cctor()][offset 0x000000CD][found Char] Unexpected type on the stack. diff --git a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_net9.0.bsl b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_net9.0.bsl index 7126edcff60..e6e4e6c9d6a 100644 --- a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_net9.0.bsl +++ b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_net9.0.bsl @@ -21,7 +21,7 @@ [IL]: Error [StackUnexpected]: : FSharp.Compiler.CodeAnalysis.Hosted.CompilerHelpers::fscCompile([FSharp.Compiler.Service]FSharp.Compiler.CodeAnalysis.LegacyReferenceResolver, string, string[])][offset 0x00000082][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.CodeAnalysis.Hosted.CompilerHelpers::fscCompile([FSharp.Compiler.Service]FSharp.Compiler.CodeAnalysis.LegacyReferenceResolver, string, string[])][offset 0x0000008B][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+MagicAssemblyResolution::ResolveAssemblyCore([FSharp.Compiler.Service]Internal.Utilities.Library.CompilationThreadToken, [FSharp.Compiler.Service]FSharp.Compiler.Text.Range, [FSharp.Compiler.Service]FSharp.Compiler.CompilerConfig+TcConfigBuilder, [FSharp.Compiler.Service]FSharp.Compiler.CompilerImports+TcImports, [FSharp.Compiler.Service]FSharp.Compiler.Interactive.Shell+FsiDynamicCompiler, [FSharp.Compiler.Service]FSharp.Compiler.Interactive.Shell+FsiConsoleOutput, string)][offset 0x00000015][found Char] Unexpected type on the stack. -[IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+clo@3494-849::Invoke([S.P.CoreLib]System.Tuple`3)][offset 0x000001C7][found Char] Unexpected type on the stack. +[IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+clo@3494-851::Invoke([S.P.CoreLib]System.Tuple`3)][offset 0x000001C7][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : .$FSharpCheckerResults+GetReferenceResolutionStructuredToolTipText@2225::Invoke([FSharp.Core]Microsoft.FSharp.Core.Unit)][offset 0x00000076][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@924-530::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x00000032][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : .$ServiceLexing+clo@924-530::Invoke([FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2,Microsoft.FSharp.Core.Unit>)][offset 0x0000003B][found Char] Unexpected type on the stack. diff --git a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl index 60128c71130..1273c54bca1 100644 --- a/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl +++ b/tests/ILVerify/ilverify_FSharp.Compiler.Service_Release_netstandard2.0.bsl @@ -28,7 +28,7 @@ [IL]: Error [StackUnexpected]: : FSharp.Compiler.CodeAnalysis.Hosted.CompilerHelpers::fscCompile([FSharp.Compiler.Service]FSharp.Compiler.CodeAnalysis.LegacyReferenceResolver, string, string[])][offset 0x0000008B][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+FsiStdinSyphon::GetLine(string, int32)][offset 0x00000032][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+MagicAssemblyResolution::ResolveAssemblyCore([FSharp.Compiler.Service]Internal.Utilities.Library.CompilationThreadToken, [FSharp.Compiler.Service]FSharp.Compiler.Text.Range, [FSharp.Compiler.Service]FSharp.Compiler.CompilerConfig+TcConfigBuilder, [FSharp.Compiler.Service]FSharp.Compiler.CompilerImports+TcImports, [FSharp.Compiler.Service]FSharp.Compiler.Interactive.Shell+FsiDynamicCompiler, [FSharp.Compiler.Service]FSharp.Compiler.Interactive.Shell+FsiConsoleOutput, string)][offset 0x00000015][found Char] Unexpected type on the stack. -[IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+clo@3494-849::Invoke([S.P.CoreLib]System.Tuple`3)][offset 0x000001C7][found Char] Unexpected type on the stack. +[IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+clo@3494-851::Invoke([S.P.CoreLib]System.Tuple`3)][offset 0x000001C7][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.Interactive.Shell+FsiInteractionProcessor::CompletionsForPartialLID([FSharp.Compiler.Service]FSharp.Compiler.Interactive.Shell+FsiDynamicCompilerState, string)][offset 0x00000024][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : .$FSharpCheckerResults+GetReferenceResolutionStructuredToolTipText@2225::Invoke([FSharp.Core]Microsoft.FSharp.Core.Unit)][offset 0x00000076][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.EditorServices.AssemblyContent+traverseMemberFunctionAndValues@176::Invoke([FSharp.Compiler.Service]FSharp.Compiler.Symbols.FSharpMemberOrFunctionOrValue)][offset 0x0000002B][found Char] Unexpected type on the stack. @@ -107,7 +107,7 @@ [IL]: Error [StackUnexpected]: : FSharp.Compiler.AbstractIL.IL::parseILVersion(string)][offset 0x0000000B][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.AbstractIL.IL::parseILVersion(string)][offset 0x00000021][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.AbstractIL.IL::parseNamed@5310(uint8[], [FSharp.Core]Microsoft.FSharp.Collections.FSharpList`1>, int32, int32)][offset 0x0000007E][found Char] Unexpected type on the stack. -[IL]: Error [StackUnexpected]: : Internal.Utilities.Collections.Utils::shortPath(string)][offset 0x00000016][found Char] Unexpected type on the stack. +[IL]: Error [StackUnexpected]: : Internal.Utilities.Collections.Utils::shortPath(string)][offset 0x0000003A][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : Internal.Utilities.FSharpEnvironment::probePathForDotnetHost@316([FSharp.Core]Microsoft.FSharp.Core.Unit)][offset 0x0000002A][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : FSharp.Compiler.CodeAnalysis.SimulatedMSBuildReferenceResolver+SimulatedMSBuildResolver@68::FSharp.Compiler.CodeAnalysis.ILegacyReferenceResolver.Resolve([FSharp.Compiler.Service]FSharp.Compiler.CodeAnalysis.LegacyResolutionEnvironment, [S.P.CoreLib]System.Tuple`2[], string, [FSharp.Core]Microsoft.FSharp.Collections.FSharpList`1, string, string, [FSharp.Core]Microsoft.FSharp.Collections.FSharpList`1, string, [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2, [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2>>)][offset 0x000002F5][found Char] Unexpected type on the stack. [IL]: Error [StackUnexpected]: : .$FSharp.Compiler.DiagnosticsLogger::.cctor()][offset 0x000000B6][found Char] Unexpected type on the stack. diff --git a/vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs b/vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs index fa17d938f10..e6e07761a75 100644 --- a/vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs +++ b/vsintegration/src/FSharp.Editor/Classification/ClassificationService.fs @@ -32,6 +32,9 @@ type SemanticClassificationLookup = IReadOnlyDictionary)>] type internal FSharpClassificationService [] () = + static let shouldProduceClassification (document: Document) = + document.Project.Solution.GetFSharpExtensionConfig().ShouldProduceSemanticHighlighting() + static let getLexicalClassifications (filePath: string, defines, text: SourceText, textSpan: TextSpan, ct: CancellationToken) = let text = text.GetSubText(textSpan) @@ -151,144 +154,154 @@ type internal FSharpClassificationService [] () = member _.AddSyntacticClassificationsAsync (document: Document, textSpan: TextSpan, result: List, cancellationToken: CancellationToken) = - cancellableTask { - use _logBlock = Logger.LogBlock(LogEditorFunctionId.Classification_Syntactic) - - let! cancellationToken = CancellableTask.getCancellationToken () - - let defines, langVersion, strictIndentation = document.GetFsharpParsingOptions() - - let! sourceText = document.GetTextAsync(cancellationToken) - - // For closed documents, only get classification for the text within the span. - // This may be inaccurate for multi-line tokens such as string literals, but this is ok for now - // as it's better than having to tokenize a big part of a file which in return will allocate a lot and hurt find all references performance. - let isOpenDocument = document.Project.Solution.Workspace.IsDocumentOpen document.Id - - let eventProps: (string * obj) array = - [| - "context.document.project.id", document.Project.Id.Id.ToString() - "context.document.id", document.Id.Id.ToString() - "isOpenDocument", isOpenDocument - "textSpanLength", textSpan.Length - |] - - use _eventDuration = - TelemetryReporter.ReportSingleEventWithDuration(TelemetryEvents.AddSyntacticClassifications, eventProps) - - if not isOpenDocument then - let classifiedSpans = - getLexicalClassifications (document.FilePath, defines, sourceText, textSpan, cancellationToken) - - result.AddRange(classifiedSpans) - else - Tokenizer.classifySpans ( - document.Id, - sourceText, - textSpan, - Some(document.FilePath), - defines, - Some langVersion, - strictIndentation, - result, - cancellationToken - ) - } - |> CancellableTask.startAsTask cancellationToken + + if not (document |> shouldProduceClassification) then + System.Threading.Tasks.Task.CompletedTask + else + + cancellableTask { + use _logBlock = Logger.LogBlock(LogEditorFunctionId.Classification_Syntactic) + + let! cancellationToken = CancellableTask.getCancellationToken () + + let defines, langVersion, strictIndentation = document.GetFsharpParsingOptions() + + let! sourceText = document.GetTextAsync(cancellationToken) + + // For closed documents, only get classification for the text within the span. + // This may be inaccurate for multi-line tokens such as string literals, but this is ok for now + // as it's better than having to tokenize a big part of a file which in return will allocate a lot and hurt find all references performance. + let isOpenDocument = document.Project.Solution.Workspace.IsDocumentOpen document.Id + + let eventProps: (string * obj) array = + [| + "context.document.project.id", document.Project.Id.Id.ToString() + "context.document.id", document.Id.Id.ToString() + "isOpenDocument", isOpenDocument + "textSpanLength", textSpan.Length + |] + + use _eventDuration = + TelemetryReporter.ReportSingleEventWithDuration(TelemetryEvents.AddSyntacticClassifications, eventProps) + + if not isOpenDocument then + let classifiedSpans = + getLexicalClassifications (document.FilePath, defines, sourceText, textSpan, cancellationToken) + + result.AddRange(classifiedSpans) + else + Tokenizer.classifySpans ( + document.Id, + sourceText, + textSpan, + Some(document.FilePath), + defines, + Some langVersion, + strictIndentation, + result, + cancellationToken + ) + } + |> CancellableTask.startAsTask cancellationToken member _.AddSemanticClassificationsAsync (document: Document, textSpan: TextSpan, result: List, cancellationToken: CancellationToken) = - cancellableTask { - use _logBlock = Logger.LogBlock(LogEditorFunctionId.Classification_Semantic) - - let! sourceText = document.GetTextAsync(cancellationToken) - - // If we are trying to get semantic classification for a document that is not open, get the results from the background and cache it. - // We do this for find all references when it is populating results. - // We cache it temporarily so we do not have to continuously call into the checker and perform a background operation. - let isOpenDocument = document.Project.Solution.Workspace.IsDocumentOpen document.Id - - if not isOpenDocument then - match! unopenedDocumentsSemanticClassificationCache.TryGetValueAsync document with - | ValueSome classificationDataLookup -> - let eventProps: (string * obj) array = - [| - "context.document.project.id", document.Project.Id.Id.ToString() - "context.document.id", document.Id.Id.ToString() - "isOpenDocument", isOpenDocument - "textSpanLength", textSpan.Length - "cacheHit", true - |] - - use _eventDuration = - TelemetryReporter.ReportSingleEventWithDuration(TelemetryEvents.AddSemanticClassifications, eventProps) - - addSemanticClassificationByLookup sourceText textSpan classificationDataLookup result - | ValueNone -> - let eventProps: (string * obj) array = - [| - "context.document.project.id", document.Project.Id.Id.ToString() - "context.document.id", document.Id.Id.ToString() - "isOpenDocument", isOpenDocument - "textSpanLength", textSpan.Length - "cacheHit", false - |] - - use _eventDuration = - TelemetryReporter.ReportSingleEventWithDuration(TelemetryEvents.AddSemanticClassifications, eventProps) - - let! classificationData = document.GetFSharpSemanticClassificationAsync(nameof (FSharpClassificationService)) - - let classificationDataLookup = toSemanticClassificationLookup classificationData - do! unopenedDocumentsSemanticClassificationCache.SetAsync(document, classificationDataLookup) - addSemanticClassificationByLookup sourceText textSpan classificationDataLookup result - else - - match! openedDocumentsSemanticClassificationCache.TryGetValueAsync document with - | ValueSome classificationDataLookup -> - let eventProps: (string * obj) array = - [| - "context.document.project.id", document.Project.Id.Id.ToString() - "context.document.id", document.Id.Id.ToString() - "isOpenDocument", isOpenDocument - "textSpanLength", textSpan.Length - "cacheHit", true - |] - - use _eventDuration = - TelemetryReporter.ReportSingleEventWithDuration(TelemetryEvents.AddSemanticClassifications, eventProps) - - addSemanticClassificationByLookup sourceText textSpan classificationDataLookup result - | ValueNone -> - - let eventProps: (string * obj) array = - [| - "context.document.project.id", document.Project.Id.Id.ToString() - "context.document.id", document.Id.Id.ToString() - "isOpenDocument", isOpenDocument - "textSpanLength", textSpan.Length - "cacheHit", false - |] - - use _eventDuration = - TelemetryReporter.ReportSingleEventWithDuration(TelemetryEvents.AddSemanticClassifications, eventProps) - - let! _, checkResults = document.GetFSharpParseAndCheckResultsAsync(nameof (IFSharpClassificationService)) - - let targetRange = - RoslynHelpers.TextSpanToFSharpRange(document.FilePath, textSpan, sourceText) - - let classificationData = checkResults.GetSemanticClassification(Some targetRange) - - if classificationData.Length > 0 then - let classificationDataLookup = itemToSemanticClassificationLookup classificationData + + if not (document |> shouldProduceClassification) then + System.Threading.Tasks.Task.CompletedTask + else + + cancellableTask { + use _logBlock = Logger.LogBlock(LogEditorFunctionId.Classification_Semantic) + + let! sourceText = document.GetTextAsync(cancellationToken) + + // If we are trying to get semantic classification for a document that is not open, get the results from the background and cache it. + // We do this for find all references when it is populating results. + // We cache it temporarily so we do not have to continuously call into the checker and perform a background operation. + let isOpenDocument = document.Project.Solution.Workspace.IsDocumentOpen document.Id + + if not isOpenDocument then + match! unopenedDocumentsSemanticClassificationCache.TryGetValueAsync document with + | ValueSome classificationDataLookup -> + let eventProps: (string * obj) array = + [| + "context.document.project.id", document.Project.Id.Id.ToString() + "context.document.id", document.Id.Id.ToString() + "isOpenDocument", isOpenDocument + "textSpanLength", textSpan.Length + "cacheHit", true + |] + + use _eventDuration = + TelemetryReporter.ReportSingleEventWithDuration(TelemetryEvents.AddSemanticClassifications, eventProps) + + addSemanticClassificationByLookup sourceText textSpan classificationDataLookup result + | ValueNone -> + let eventProps: (string * obj) array = + [| + "context.document.project.id", document.Project.Id.Id.ToString() + "context.document.id", document.Id.Id.ToString() + "isOpenDocument", isOpenDocument + "textSpanLength", textSpan.Length + "cacheHit", false + |] + + use _eventDuration = + TelemetryReporter.ReportSingleEventWithDuration(TelemetryEvents.AddSemanticClassifications, eventProps) + + let! classificationData = document.GetFSharpSemanticClassificationAsync(nameof (FSharpClassificationService)) + + let classificationDataLookup = toSemanticClassificationLookup classificationData do! unopenedDocumentsSemanticClassificationCache.SetAsync(document, classificationDataLookup) + addSemanticClassificationByLookup sourceText textSpan classificationDataLookup result + else - addSemanticClassification sourceText textSpan classificationData result - } - |> CancellableTask.ifCanceledReturn () - |> CancellableTask.startAsTask cancellationToken + match! openedDocumentsSemanticClassificationCache.TryGetValueAsync document with + | ValueSome classificationDataLookup -> + let eventProps: (string * obj) array = + [| + "context.document.project.id", document.Project.Id.Id.ToString() + "context.document.id", document.Id.Id.ToString() + "isOpenDocument", isOpenDocument + "textSpanLength", textSpan.Length + "cacheHit", true + |] + + use _eventDuration = + TelemetryReporter.ReportSingleEventWithDuration(TelemetryEvents.AddSemanticClassifications, eventProps) + + addSemanticClassificationByLookup sourceText textSpan classificationDataLookup result + | ValueNone -> + + let eventProps: (string * obj) array = + [| + "context.document.project.id", document.Project.Id.Id.ToString() + "context.document.id", document.Id.Id.ToString() + "isOpenDocument", isOpenDocument + "textSpanLength", textSpan.Length + "cacheHit", false + |] + + use _eventDuration = + TelemetryReporter.ReportSingleEventWithDuration(TelemetryEvents.AddSemanticClassifications, eventProps) + + let! _, checkResults = document.GetFSharpParseAndCheckResultsAsync(nameof (IFSharpClassificationService)) + + let targetRange = + RoslynHelpers.TextSpanToFSharpRange(document.FilePath, textSpan, sourceText) + + let classificationData = checkResults.GetSemanticClassification(Some targetRange) + + if classificationData.Length > 0 then + let classificationDataLookup = itemToSemanticClassificationLookup classificationData + do! unopenedDocumentsSemanticClassificationCache.SetAsync(document, classificationDataLookup) + + addSemanticClassification sourceText textSpan classificationData result + } + |> CancellableTask.ifCanceledReturn () + |> CancellableTask.startAsTask cancellationToken // Do not perform classification if we don't have project options (#defines matter) member _.AdjustStaleClassification(_: SourceText, classifiedSpan: ClassifiedSpan) : ClassifiedSpan = classifiedSpan diff --git a/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs b/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs index 78a28eb0de2..4299936d05a 100644 --- a/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs +++ b/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs @@ -336,7 +336,7 @@ module CancellableTasks = else let mutable awaiter = sm.ResumptionDynamicInfo.ResumptionData - :?> ICriticalNotifyCompletion + :?> ICriticalNotifyCompletion | null assert not (isNull awaiter) sm.Data.MethodBuilder.AwaitUnsafeOnCompleted(&awaiter, &sm) @@ -386,7 +386,7 @@ module CancellableTasks = (MoveNextMethodImpl<_>(fun sm -> //-- RESUMABLE CODE START __resumeAt sm.ResumptionPoint - let mutable __stack_exn: Exception = null + let mutable __stack_exn: Exception | null = null try let __stack_code_fin = code.Invoke(&sm) diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs index 36eb5bc7a0e..b7f7ed3d4cf 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/DocumentDiagnosticAnalyzer.fs @@ -24,6 +24,9 @@ type internal DiagnosticsType = [)>] type internal FSharpDocumentDiagnosticAnalyzer [] () = + let shouldProduceDiagnostics (document: Document) = + document.Project.Solution.GetFSharpExtensionConfig().ShouldProduceDiagnostics() + static let diagnosticEqualityComparer = { new IEqualityComparer with @@ -148,14 +151,20 @@ type internal FSharpDocumentDiagnosticAnalyzer [] () = interface IFSharpDocumentDiagnosticAnalyzer with member _.AnalyzeSyntaxAsync(document: Document, cancellationToken: CancellationToken) : Task> = - if document.Project.IsFSharpMetadata then + if + document.Project.IsFSharpMetadata + || (not (document |> shouldProduceDiagnostics)) + then Task.FromResult ImmutableArray.Empty else FSharpDocumentDiagnosticAnalyzer.GetDiagnostics(document, DiagnosticsType.Syntax) |> CancellableTask.start cancellationToken member _.AnalyzeSemanticsAsync(document: Document, cancellationToken: CancellationToken) : Task> = - if document.Project.IsFSharpMiscellaneousOrMetadata && not document.IsFSharpScript then + if + document.Project.IsFSharpMiscellaneousOrMetadata && not document.IsFSharpScript + || (not (document |> shouldProduceDiagnostics)) + then Task.FromResult ImmutableArray.Empty else FSharpDocumentDiagnosticAnalyzer.GetDiagnostics(document, DiagnosticsType.Semantic) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs index 1c09806d0ca..518bf88bc74 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs @@ -15,6 +15,8 @@ open FSharp.Compiler.BuildGraph open CancellableTasks +open System.IO + open Internal.Utilities.Collections open Newtonsoft.Json open Newtonsoft.Json.Linq @@ -31,12 +33,72 @@ module internal ProjectCache = let Projects = ConditionalWeakTable() +module internal SolutionConfigCache = + + type FSharpExtensionConfig = + { + GetDiagnosticsFrom: string + GetSemanticHighlightingFrom: string + } + + static member Old = "old" + static member Lsp = "lsp" + static member Both = "both" + + static member Default = + { + GetDiagnosticsFrom = FSharpExtensionConfig.Both + GetSemanticHighlightingFrom = FSharpExtensionConfig.Both + } + + member this.ShouldProduceDiagnostics() = + Set.contains this.GetDiagnosticsFrom (set [ FSharpExtensionConfig.Old; FSharpExtensionConfig.Both ]) + + member this.ShouldProduceSemanticHighlighting() = + Set.contains this.GetSemanticHighlightingFrom (set [ FSharpExtensionConfig.Old; FSharpExtensionConfig.Both ]) + + let readFSharpExtensionConfig (solutionPath: string) = + if String.IsNullOrEmpty(solutionPath) then + System.Diagnostics.Trace.TraceWarning("Solution path is null or empty. Using default config.") + FSharpExtensionConfig.Default + else + let configFilePath = + Path.Combine(solutionPath, "extensibility.settings.VisualStudio.json") + + if File.Exists configFilePath then + try + let json = File.ReadAllText configFilePath + let jObject = JObject.Parse json + + { + GetDiagnosticsFrom = jObject["fsharp.getDiagnosticsFrom"].ToString().ToLower() + GetSemanticHighlightingFrom = jObject["fsharp.getSemanticHighlightingFrom"].ToString().ToLower() + } + with ex -> + System.Diagnostics.Trace.TraceError($"Error reading FSharpExtensionConfig from {configFilePath}", ex) + FSharpExtensionConfig.Default + else + System.Diagnostics.Trace.TraceInformation( + $"extensibility.settings.VisualStudio.json not found in {solutionPath}. Using default config." + ) + + FSharpExtensionConfig.Default + + let ExtensionConfig = ConditionalWeakTable() + type Solution with /// Get the instance of IFSharpWorkspaceService. member internal this.GetFSharpWorkspaceService() = this.Workspace.Services.GetRequiredService() + member internal this.GetFSharpExtensionConfig() = + SolutionConfigCache.ExtensionConfig.GetValue( + this, + ConditionalWeakTable<_, _>.CreateValueCallback(fun _ -> + SolutionConfigCache.readFSharpExtensionConfig (Path.GetDirectoryName this.FilePath)) + ) + module internal FSharpProjectSnapshotSerialization = let serializeFileSnapshot (snapshot: FSharpFileSnapshot) =