diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index b98681526..4896ad695 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -27,10 +27,10 @@ ] }, "fantomas": { - "version": "6.0.0", + "version": "6.1.0", "commands": [ "fantomas" ] } } -} \ No newline at end of file +} diff --git a/.gitignore b/.gitignore index 77bf793bb..48f5e612c 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ coverage test/FsAutoComplete.Tests.Lsp/TestResults/ .tool-versions +BenchmarkDotNet.Artifacts/ diff --git a/FsAutoComplete.sln b/FsAutoComplete.sln index 7bc5d8f13..5aed2b731 100644 --- a/FsAutoComplete.sln +++ b/FsAutoComplete.sln @@ -27,6 +27,8 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FsAutoComplete.DependencyMa EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "build", "build\build.fsproj", "{400D56D0-28C9-4210-AA30-BD688122E298}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "benchmarks", "benchmarks\benchmarks.fsproj", "{0CD029D8-B39E-4CBE-A190-C84A7A811180}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -65,6 +67,10 @@ Global {400D56D0-28C9-4210-AA30-BD688122E298}.Debug|Any CPU.Build.0 = Debug|Any CPU {400D56D0-28C9-4210-AA30-BD688122E298}.Release|Any CPU.ActiveCfg = Release|Any CPU {400D56D0-28C9-4210-AA30-BD688122E298}.Release|Any CPU.Build.0 = Release|Any CPU + {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/benchmarks/Program.fs b/benchmarks/Program.fs new file mode 100644 index 000000000..a4eb20197 --- /dev/null +++ b/benchmarks/Program.fs @@ -0,0 +1,16 @@ +namespace Benchmarks +open System +open BenchmarkDotNet +open BenchmarkDotNet.Attributes +open BenchmarkDotNet.Running +open System.Security.Cryptography + + + + +module EntryPoint = + + [] + let main argv = + let summary = BenchmarkRunner.Run(); + 0 diff --git a/benchmarks/SourceTextBenchmarks.fs b/benchmarks/SourceTextBenchmarks.fs new file mode 100644 index 000000000..cbaf7e795 --- /dev/null +++ b/benchmarks/SourceTextBenchmarks.fs @@ -0,0 +1,69 @@ +namespace Benchmarks + +open System +open FSharp.Data.Adaptive +open Microsoft.CodeAnalysis.Text +type FileVersion = int + + +module Helpers = + open FsAutoComplete.LspHelpers + open FSharp.UMX + open System.Collections.Generic + + let fileContents = IO.File.ReadAllText(@"C:\Users\jimmy\Repositories\public\TheAngryByrd\span-playground\Romeo and Juliet by William Shakespeare.txt") + + let initNamedText () = + FsAutoComplete.NamedText(UMX.tag "lol", fileContents) + + let initRoslynSourceText () = + SourceText.From(fileContents) + + + let convertToTextSpan (sourceText : SourceText, range : Ionide.LanguageServerProtocol.Types.Range) = + let start = sourceText.Lines.[max 0 (range.Start.Line)].Start + range.Start.Character + let endPosition = + sourceText.Lines.[min (range.End.Line) (sourceText.Lines.Count - 1)].Start + + range.End.Character + TextSpan(start, endPosition - start) + + let addToSourceText (sourceText : SourceText, range : Ionide.LanguageServerProtocol.Types.Range, text : string) = + let textSpan = convertToTextSpan(sourceText, range) + let newText = sourceText.WithChanges([| TextChange(textSpan, text) |]) + newText + + let addToSourceTextMany (sourceText : SourceText, spans : IEnumerable) = + let textSpans = spans |> Seq.map (fun (range, text) -> TextChange(convertToTextSpan(sourceText, range), text)) |> Seq.toArray + let newText = sourceText.WithChanges(textSpans) + newText + + let addToNamedText (namedText : FsAutoComplete.NamedText, range : Ionide.LanguageServerProtocol.Types.Range, text : string) = + let range = protocolRangeToRange (UMX.untag namedText.FileName) range + match namedText.ModifyText(range, text) with | Ok x -> x | Error e -> failwith e + +open BenchmarkDotNet +open BenchmarkDotNet.Attributes +open Helpers +open BenchmarkDotNet.Jobs +[] +[] +type SourceText_LineChanges_Benchmarks ()= + + [] + member val public N = 0 with get, set + + [] + member this.Named_Text_changeText_everyUpdate () = + let mutable file = initNamedText () + file <- addToNamedText(file, { Start = { Line = 0; Character = 5 }; End = { Line = 0; Character = 5 } }, "World") + for i in 1..this.N do + file <- addToNamedText(file, { Start = { Line = 0; Character = 10 }; End = { Line = 0; Character = 10 } }, "\nLOL") + file.Lines |> Seq.toArray |> ignore + + [] + member this.Roslyn_Text_changeText_everyUpdate () = + let mutable file = initRoslynSourceText () + file <- addToSourceText(file, { Start = { Line = 0; Character = 5 }; End = { Line = 0; Character = 5 } }, "World") + for i in 1..this.N do + file <- addToSourceText(file, { Start = { Line = 0; Character = 10 }; End = { Line = 0; Character = 10 } }, "\nLOL") + file.Lines |> Seq.toArray |> ignore diff --git a/benchmarks/benchmarks.fsproj b/benchmarks/benchmarks.fsproj new file mode 100644 index 000000000..6b0f89e87 --- /dev/null +++ b/benchmarks/benchmarks.fsproj @@ -0,0 +1,15 @@ + + + + Exe + net6.0;net7.0 + + + + + + + + + + diff --git a/benchmarks/paket.references b/benchmarks/paket.references new file mode 100644 index 000000000..d29cc1cf0 --- /dev/null +++ b/benchmarks/paket.references @@ -0,0 +1,4 @@ +FSharp.Core +BenchmarkDotNet +Microsoft.CodeAnalysis +FSharp.Data.Adaptive diff --git a/paket.dependencies b/paket.dependencies index 6e4604584..bacbcf507 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -11,6 +11,7 @@ strategy: min lowest_matching: true +nuget BenchmarkDotNet 0.13.5 nuget Fantomas.Client >= 0.9 nuget FSharp.Compiler.Service >= 43.7.300 nuget Ionide.ProjInfo >= 0.61.3 @@ -21,6 +22,7 @@ nuget Microsoft.Build >= 17.2 copy_local:false nuget Microsoft.Build.Framework >= 17.4 copy_local:false nuget Microsoft.Build.Utilities.Core >= 17.4 copy_local:false nuget Microsoft.Build.Tasks.Core >= 17.4 copy_local: false +nuget Microsoft.CodeAnalysis 4.5.0 nuget Nuget.Frameworks copy_local: false nuget FSharp.Analyzers.SDK nuget ICSharpCode.Decompiler diff --git a/paket.lock b/paket.lock index db6a40904..056ccc40c 100644 --- a/paket.lock +++ b/paket.lock @@ -5,11 +5,29 @@ RESTRICTION: || (== net6.0) (== net7.0) (== netstandard2.0) (== netstandard2.1) NUGET remote: https://api.nuget.org/v3/index.json altcover (8.3.838) + BenchmarkDotNet (0.13.5) + BenchmarkDotNet.Annotations (>= 0.13.5) + CommandLineParser (>= 2.4.3) + Gee.External.Capstone (>= 2.3) + Iced (>= 1.17) + Microsoft.CodeAnalysis.CSharp (>= 3.0) + Microsoft.Diagnostics.Runtime (>= 2.2.332302) + Microsoft.Diagnostics.Tracing.TraceEvent (>= 3.0.2) + Microsoft.DotNet.PlatformAbstractions (>= 3.1.6) + Microsoft.Win32.Registry (>= 5.0) - restriction: || (&& (== net7.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) + Perfolizer (>= 0.2.1) + System.Management (>= 6.0) + System.Numerics.Vectors (>= 4.5) - restriction: || (&& (== net7.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) + System.Reflection.Emit (>= 4.7) - restriction: || (&& (== net7.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) + System.Reflection.Emit.Lightweight (>= 4.7) - restriction: || (&& (== net7.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) + System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: || (&& (== net7.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) + BenchmarkDotNet.Annotations (0.13.5) CliWrap (3.4.4) Microsoft.Bcl.AsyncInterfaces (>= 6.0) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netstandard2.1)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< netstandard2.1)) (== netstandard2.0) (&& (== netstandard2.1) (>= net461)) System.Buffers (>= 4.5.1) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netstandard2.1)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< netstandard2.1)) (== netstandard2.0) (&& (== netstandard2.1) (>= net461)) System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netstandard2.1)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< netstandard2.1)) (== netstandard2.0) (&& (== netstandard2.1) (>= net461)) System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netstandard2.1)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< netstandard2.1)) (== netstandard2.0) (&& (== netstandard2.1) (>= net461)) + CommandLineParser (2.4.3) Destructurama.FSharp (1.2) FSharp.Core (>= 4.3.4) Serilog (>= 2.0 < 3.0) @@ -79,6 +97,7 @@ NUGET FSharp.Core (>= 7.0) - restriction: || (== net6.0) (== net7.0) (&& (== netstandard2.0) (>= netstandard2.1)) (== netstandard2.1) FsToolkit.ErrorHandling.TaskResult (4.4) - restriction: || (== net6.0) (== net7.0) (== netstandard2.1) FsToolkit.ErrorHandling (>= 4.4) + Gee.External.Capstone (2.3) GitHubActionsTestLogger (2.0.1) Microsoft.TestPlatform.ObjectModel (>= 17.2) Google.Protobuf (3.22) @@ -97,6 +116,8 @@ NUGET System.Diagnostics.DiagnosticSource (>= 4.5.1) - restriction: || (&& (== net6.0) (< net5.0)) (&& (== net6.0) (< netstandard2.1)) (&& (== net7.0) (< net5.0)) (&& (== net7.0) (< netstandard2.1)) (== netstandard2.0) (== netstandard2.1) Grpc.Net.Common (2.51) - restriction: || (== net6.0) (== net7.0) (&& (== netstandard2.0) (>= netstandard2.1)) (== netstandard2.1) Grpc.Core.Api (>= 2.51) + Humanizer.Core (2.14.1) + Iced (1.17) IcedTasks (0.5.3) FSharp.Core (>= 7.0) ICSharpCode.Decompiler (7.2.1.6856) @@ -178,8 +199,49 @@ NUGET System.Configuration.ConfigurationManager (>= 6.0) System.Security.Permissions (>= 6.0) - restriction: || (== net6.0) (== netstandard2.0) (== netstandard2.1) System.Text.Encoding.CodePages (>= 6.0) - restriction: || (== net6.0) (== netstandard2.0) (== netstandard2.1) + Microsoft.CodeAnalysis (4.5) + Microsoft.CodeAnalysis.CSharp.Workspaces (4.5) + Microsoft.CodeAnalysis.VisualBasic.Workspaces (4.5) + Microsoft.CodeAnalysis.Analyzers (3.3.3) + Microsoft.CodeAnalysis.Common (4.5) + Microsoft.CodeAnalysis.Analyzers (>= 3.3.3) + System.Collections.Immutable (>= 6.0) + System.Memory (>= 4.5.5) - restriction: || (&& (== net6.0) (< netcoreapp3.1)) (&& (== net7.0) (< netcoreapp3.1)) (== netstandard2.0) (== netstandard2.1) + System.Reflection.Metadata (>= 6.0.1) + System.Runtime.CompilerServices.Unsafe (>= 6.0) + System.Text.Encoding.CodePages (>= 6.0) + System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: || (&& (== net6.0) (< netcoreapp3.1)) (&& (== net7.0) (< netcoreapp3.1)) (== netstandard2.0) (== netstandard2.1) + Microsoft.CodeAnalysis.CSharp (4.5) + Microsoft.CodeAnalysis.Common (4.5) + Microsoft.CodeAnalysis.CSharp.Workspaces (4.5) + Humanizer.Core (>= 2.14.1) + Microsoft.CodeAnalysis.Common (4.5) + Microsoft.CodeAnalysis.CSharp (4.5) + Microsoft.CodeAnalysis.Workspaces.Common (4.5) + Microsoft.CodeAnalysis.VisualBasic (4.5) + Microsoft.CodeAnalysis.Common (4.5) + Microsoft.CodeAnalysis.VisualBasic.Workspaces (4.5) + Microsoft.CodeAnalysis.Common (4.5) + Microsoft.CodeAnalysis.VisualBasic (4.5) + Microsoft.CodeAnalysis.Workspaces.Common (4.5) + Microsoft.CodeAnalysis.Workspaces.Common (4.5) + Humanizer.Core (>= 2.14.1) + Microsoft.Bcl.AsyncInterfaces (>= 6.0) + Microsoft.CodeAnalysis.Common (4.5) + System.Composition (>= 6.0) + System.IO.Pipelines (>= 6.0.3) + System.Threading.Channels (>= 6.0) Microsoft.CodeCoverage (17.4.1) - restriction: || (== net6.0) (== net7.0) (&& (== netstandard2.0) (>= net462)) (&& (== netstandard2.0) (>= netcoreapp3.1)) (&& (== netstandard2.1) (>= net462)) (&& (== netstandard2.1) (>= netcoreapp3.1)) - Microsoft.DotNet.PlatformAbstractions (3.1.6) - restriction: || (== net6.0) (== net7.0) (&& (== netstandard2.0) (>= net5.0)) (&& (== netstandard2.1) (>= net5.0)) + Microsoft.Diagnostics.NETCore.Client (0.2.251802) + Microsoft.Bcl.AsyncInterfaces (>= 1.1) + Microsoft.Extensions.Logging (>= 2.1.1) + Microsoft.Diagnostics.Runtime (2.2.332302) + Microsoft.Diagnostics.NETCore.Client (>= 0.2.251802) + System.Collections.Immutable (>= 5.0) + System.Runtime.CompilerServices.Unsafe (>= 5.0) + Microsoft.Diagnostics.Tracing.TraceEvent (3.0.2) + System.Runtime.CompilerServices.Unsafe (>= 4.5.3) + Microsoft.DotNet.PlatformAbstractions (3.1.6) Microsoft.Extensions.Caching.Abstractions (6.0) Microsoft.Extensions.Primitives (>= 6.0) Microsoft.Extensions.Caching.Memory (6.0.1) @@ -304,6 +366,8 @@ NUGET Grpc (>= 2.44 < 3.0) - restriction: || (&& (== net6.0) (>= net462)) (&& (== net6.0) (< netstandard2.1)) (&& (== net7.0) (>= net462)) (&& (== net7.0) (< netstandard2.1)) (== netstandard2.0) (&& (== netstandard2.1) (>= net462)) Grpc.Net.Client (>= 2.43 < 3.0) - restriction: || (== net6.0) (== net7.0) (&& (== netstandard2.0) (>= netstandard2.1)) (== netstandard2.1) OpenTelemetry (>= 1.3.2) + Perfolizer (0.2.1) + System.Memory (>= 4.5.3) SemanticVersioning (2.0.2) Serilog (2.11) Serilog.Sinks.Async (1.5) @@ -330,6 +394,22 @@ NUGET System.CommandLine (2.0.0-beta4.22272.1) System.Memory (>= 4.5.4) - restriction: || (&& (== net7.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) System.ComponentModel.Annotations (5.0) - restriction: || (&& (== net6.0) (< netstandard2.1)) (&& (== net7.0) (< netstandard2.1)) (== netstandard2.0) + System.Composition (6.0) + System.Composition.AttributedModel (>= 6.0) + System.Composition.Convention (>= 6.0) + System.Composition.Hosting (>= 6.0) + System.Composition.Runtime (>= 6.0) + System.Composition.TypedParts (>= 6.0) + System.Composition.AttributedModel (6.0) + System.Composition.Convention (6.0) + System.Composition.AttributedModel (>= 6.0) + System.Composition.Hosting (6.0) + System.Composition.Runtime (>= 6.0) + System.Composition.Runtime (6.0) + System.Composition.TypedParts (6.0) + System.Composition.AttributedModel (>= 6.0) + System.Composition.Hosting (>= 6.0) + System.Composition.Runtime (>= 6.0) System.Configuration.ConfigurationManager (6.0) System.Security.Cryptography.ProtectedData (>= 6.0) System.Security.Permissions (>= 6.0) @@ -345,15 +425,17 @@ NUGET System.Buffers (>= 4.5.1) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netcoreapp3.1)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< netcoreapp3.1)) (== netstandard2.0) (== netstandard2.1) System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netcoreapp3.1)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< netcoreapp3.1)) (== netstandard2.0) (== netstandard2.1) System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netcoreapp3.1)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< netcoreapp3.1)) (== netstandard2.0) (== netstandard2.1) + System.Management (6.0) + System.CodeDom (>= 6.0) System.Memory (4.5.5) System.Buffers (>= 4.5.1) - restriction: || (&& (== net6.0) (>= monotouch)) (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netcoreapp2.0)) (&& (== net6.0) (< netstandard1.1)) (&& (== net6.0) (< netstandard2.0)) (&& (== net6.0) (>= xamarinios)) (&& (== net6.0) (>= xamarinmac)) (&& (== net6.0) (>= xamarintvos)) (&& (== net6.0) (>= xamarinwatchos)) (&& (== net7.0) (>= monotouch)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< netcoreapp2.0)) (&& (== net7.0) (< netstandard1.1)) (&& (== net7.0) (< netstandard2.0)) (&& (== net7.0) (>= xamarinios)) (&& (== net7.0) (>= xamarinmac)) (&& (== net7.0) (>= xamarintvos)) (&& (== net7.0) (>= xamarinwatchos)) (== netstandard2.0) (== netstandard2.1) System.Numerics.Vectors (>= 4.4) - restriction: || (&& (== net6.0) (< netcoreapp2.0)) (&& (== net7.0) (< netcoreapp2.0)) (== netstandard2.0) (== netstandard2.1) System.Runtime.CompilerServices.Unsafe (>= 4.5.3) - restriction: || (&& (== net6.0) (>= monotouch)) (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netcoreapp2.0)) (&& (== net6.0) (< netcoreapp2.1)) (&& (== net6.0) (< netstandard1.1)) (&& (== net6.0) (< netstandard2.0)) (&& (== net6.0) (>= uap10.1)) (&& (== net6.0) (>= xamarinios)) (&& (== net6.0) (>= xamarinmac)) (&& (== net6.0) (>= xamarintvos)) (&& (== net6.0) (>= xamarinwatchos)) (&& (== net7.0) (>= monotouch)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< netcoreapp2.0)) (&& (== net7.0) (< netcoreapp2.1)) (&& (== net7.0) (< netstandard1.1)) (&& (== net7.0) (< netstandard2.0)) (&& (== net7.0) (>= uap10.1)) (&& (== net7.0) (>= xamarinios)) (&& (== net7.0) (>= xamarinmac)) (&& (== net7.0) (>= xamarintvos)) (&& (== net7.0) (>= xamarinwatchos)) (== netstandard2.0) (== netstandard2.1) - System.Numerics.Vectors (4.5) - restriction: || (&& (== net6.0) (< netcoreapp2.0)) (&& (== net7.0) (< netcoreapp2.0)) (== netstandard2.0) (== netstandard2.1) + System.Numerics.Vectors (4.5) - restriction: || (&& (== net7.0) (< net6.0)) (== netstandard2.0) (== netstandard2.1) System.Reactive (5.0) - restriction: || (== net6.0) (== net7.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) System.Reflection.Emit (4.7) System.Reflection.Emit.ILGeneration (>= 4.7) - restriction: || (&& (== net6.0) (< netcoreapp2.0) (< netstandard2.1)) (&& (== net6.0) (< netstandard1.1)) (&& (== net6.0) (< netstandard2.0)) (&& (== net6.0) (>= uap10.1)) (&& (== net7.0) (< netcoreapp2.0) (< netstandard2.1)) (&& (== net7.0) (< netstandard1.1)) (&& (== net7.0) (< netstandard2.0)) (&& (== net7.0) (>= uap10.1)) (== netstandard2.0) (&& (== netstandard2.1) (< netstandard1.1)) (&& (== netstandard2.1) (< netstandard2.0)) (&& (== netstandard2.1) (>= uap10.1)) - System.Reflection.Emit.ILGeneration (4.7) - restriction: || (&& (== net6.0) (< netcoreapp2.0) (< netstandard2.1)) (&& (== net6.0) (< netstandard1.1)) (&& (== net6.0) (< netstandard2.0)) (&& (== net6.0) (>= uap10.1)) (&& (== net7.0) (< netcoreapp2.0) (< netstandard2.1)) (&& (== net7.0) (< netstandard1.1)) (&& (== net7.0) (< netstandard2.0)) (&& (== net7.0) (>= uap10.1)) (== netstandard2.0) (&& (== netstandard2.1) (< netstandard1.1)) (&& (== netstandard2.1) (< netstandard2.0)) (&& (== netstandard2.1) (>= uap10.1)) + System.Reflection.Emit.ILGeneration (4.7) - restriction: || (&& (== net7.0) (< netcoreapp2.0) (< netstandard2.1)) (&& (== net7.0) (< netstandard1.1)) (&& (== net7.0) (< netstandard2.0)) (&& (== net7.0) (>= uap10.1)) (== netstandard2.0) (&& (== netstandard2.1) (< netstandard1.1)) (&& (== netstandard2.1) (< netstandard2.0)) (&& (== netstandard2.1) (>= uap10.1)) System.Reflection.Emit.Lightweight (4.7) System.Reflection.Emit.ILGeneration (>= 4.7) - restriction: || (&& (== net6.0) (< netcoreapp2.0) (< netstandard2.1)) (&& (== net6.0) (< netstandard2.0)) (&& (== net6.0) (< portable-net45+wp8)) (&& (== net6.0) (>= uap10.1)) (&& (== net7.0) (< netcoreapp2.0) (< netstandard2.1)) (&& (== net7.0) (< netstandard2.0)) (&& (== net7.0) (< portable-net45+wp8)) (&& (== net7.0) (>= uap10.1)) (== netstandard2.0) (&& (== netstandard2.1) (< netstandard2.0)) (&& (== netstandard2.1) (< portable-net45+wp8)) (&& (== netstandard2.1) (>= uap10.1)) System.Reflection.Metadata (6.0.1) @@ -387,6 +469,8 @@ NUGET System.Text.Json (6.0.5) - copy_local: false, restriction: || (== net6.0) (== net7.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net472)) (&& (== netstandard2.1) (>= net6.0)) System.Runtime.CompilerServices.Unsafe (>= 6.0) System.Text.Encodings.Web (>= 6.0) + System.Threading.Channels (6.0) + System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netstandard2.1)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< netstandard2.1)) (== netstandard2.0) (&& (== netstandard2.1) (>= net461)) System.Threading.Tasks.Dataflow (6.0) - copy_local: false System.Threading.Tasks.Extensions (4.5.4) System.Runtime.CompilerServices.Unsafe (>= 4.5.3) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netcoreapp2.1)) (&& (== net6.0) (< netstandard1.0)) (&& (== net6.0) (< netstandard2.0)) (&& (== net6.0) (>= wp8)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< netcoreapp2.1)) (&& (== net7.0) (< netstandard1.0)) (&& (== net7.0) (< netstandard2.0)) (&& (== net7.0) (>= wp8)) (== netstandard2.0) (== netstandard2.1) diff --git a/src/FsAutoComplete.Core/AbstractClassStubGenerator.fs b/src/FsAutoComplete.Core/AbstractClassStubGenerator.fs index c0b3c31c3..f6183cf02 100644 --- a/src/FsAutoComplete.Core/AbstractClassStubGenerator.fs +++ b/src/FsAutoComplete.Core/AbstractClassStubGenerator.fs @@ -96,7 +96,7 @@ let private tryFindAbstractClassExprInParsedInput let tryFindAbstractClassExprInBufferAtPos (codeGenService: ICodeGenerationService) (pos: Position) - (document: NamedText) + (document: IFSACSourceText) = asyncMaybe { let! parseResults = codeGenService.ParseFileInProject document.FileName @@ -151,7 +151,7 @@ let inferStartColumn let writeAbstractClassStub (codeGenServer: ICodeGenerationService) (checkResultForFile: ParseAndCheckResults) - (doc: NamedText) + (doc: IFSACSourceText) (lineStr: string) (abstractClassData: AbstractClassData) = diff --git a/src/FsAutoComplete.Core/AdaptiveExtensions.fs b/src/FsAutoComplete.Core/AdaptiveExtensions.fs index 06cfc0dbf..d294a03e7 100644 --- a/src/FsAutoComplete.Core/AdaptiveExtensions.fs +++ b/src/FsAutoComplete.Core/AdaptiveExtensions.fs @@ -32,6 +32,14 @@ module AdaptiveExtensions = | None -> x.Add(key, adder key) |> ignore | Some v -> updater key v + member x.GetOrAdd(key, adder: 'Key -> 'Value) : 'Value = + match x.TryGetValue key with + | Some x -> x + | None -> + let v = adder key + x.Add(key, v) |> ignore + v + module Utils = let cheapEqual (a: 'T) (b: 'T) = @@ -533,7 +541,7 @@ module AsyncAVal = /// - /// Returns a new async adaptive value that adaptively applies the mapping fun tion to the given + /// Returns a new async adaptive value that adaptively applies the mapping function to the given /// adaptive inputs. /// let map (mapping: 'a -> CancellationToken -> Task<'b>) (input: asyncaval<'a>) = @@ -565,7 +573,7 @@ module AsyncAVal = /// - /// Returns a new async adaptive value that adaptively applies the mapping fun tion to the given + /// Returns a new async adaptive value that adaptively applies the mapping function to the given /// adaptive inputs. /// let mapAsync (mapping: 'a -> Async<'b>) (input: asyncaval<'a>) = @@ -597,7 +605,7 @@ module AsyncAVal = /// - /// Returns a new async adaptive value that adaptively applies the mapping fun tion to the given + /// Returns a new async adaptive value that adaptively applies the mapping function to the given /// adaptive inputs. /// let mapSync (mapping: 'a -> CancellationToken -> 'b) (input: asyncaval<'a>) = diff --git a/src/FsAutoComplete.Core/Commands.fs b/src/FsAutoComplete.Core/Commands.fs index ca6f93544..aec4aabfa 100644 --- a/src/FsAutoComplete.Core/Commands.fs +++ b/src/FsAutoComplete.Core/Commands.fs @@ -44,8 +44,8 @@ type CoreResponse<'a> = [] type FormatDocumentResponse = - | Formatted of source: NamedText * formatted: string - | FormattedRange of source: NamedText * formatted: string * range: FormatSelectionRange + | Formatted of source: IFSACSourceText * formatted: string + | FormattedRange of source: IFSACSourceText * formatted: string * range: FormatSelectionRange | UnChanged | Ignored | ToolNotPresent @@ -210,20 +210,19 @@ module Commands = } let scopesForFile - (getParseResultsForFile: _ -> Async>) + (getParseResultsForFile: _ -> Async>) (file: string) = asyncResult { let! (text, ast) = getParseResultsForFile file - let ranges = - Structure.getOutliningRanges (text.ToString().Split("\n")) ast.ParseTree + let ranges = Structure.getOutliningRanges (text.Lines) ast.ParseTree return ranges } - let docForText (lines: NamedText) (tyRes: ParseAndCheckResults) : Document = + let docForText (lines: IFSACSourceText) (tyRes: ParseAndCheckResults) : Document = { LineCount = lines.Lines.Length FullName = tyRes.FileName // from the compiler, assumed safe GetText = fun _ -> string lines @@ -235,7 +234,7 @@ module Commands = writeAbstractClassStub (tyRes: ParseAndCheckResults) (objExprRange: Range) - (lines: NamedText) + (lines: IFSACSourceText) (lineStr: LineStr) = asyncResult { @@ -254,7 +253,7 @@ module Commands = tryFindRecordDefinitionFromPos (tyRes: ParseAndCheckResults) (pos: Position) - (lines: NamedText) + (lines: IFSACSourceText) (line: LineStr) = async { @@ -396,7 +395,7 @@ module Commands = } let formatSelection - (tryGetFileCheckerOptionsWithLines: _ -> Async>) + (tryGetFileCheckerOptionsWithLines: _ -> Async>) (formatSelectionAsync: _ -> System.Threading.Tasks.Task) (file: string) (rangeToFormat: FormatSelectionRange) @@ -453,7 +452,7 @@ module Commands = } let formatDocument - (tryGetFileCheckerOptionsWithLines: _ -> Async>) + (tryGetFileCheckerOptionsWithLines: _ -> Async>) (formatDocumentAsync: _ -> System.Threading.Tasks.Task) (file: string) : Async> = @@ -546,7 +545,7 @@ module Commands = |> Result.bimap CoreResponse.Res CoreResponse.ErrorRes // Calculates pipeline hints for now as in fSharp/pipelineHint with a bit of formatting on the hints - let inlineValues (contents: NamedText) (tyRes: ParseAndCheckResults) : Async<(pos * String)[]> = + let inlineValues (contents: IFSACSourceText) (tyRes: ParseAndCheckResults) : Async<(pos * String)[]> = asyncResult { // Debug.waitForDebuggerAttached "AdaptiveServer" let getSignatureAtPos pos = @@ -617,7 +616,7 @@ module Commands = |> AsyncResult.foldResult id (fun _ -> [||]) - let pipelineHints (tryGetFileSource: _ -> Async>) (tyRes: ParseAndCheckResults) = + let pipelineHints (tryGetFileSource: _ -> Async>) (tyRes: ParseAndCheckResults) = asyncResult { // Debug.waitForDebuggerAttached "AdaptiveServer" let! contents = tryGetFileSource tyRes.FileName @@ -760,9 +759,9 @@ module Commands = /// * When exact ranges are required /// -> for "Rename" let symbolUseWorkspace - (getDeclarationLocation: FSharpSymbolUse * NamedText -> Async) + (getDeclarationLocation: FSharpSymbolUse * IFSACSourceText -> Async) (findReferencesForSymbolInFile: (string * FSharpProjectOptions * FSharpSymbol) -> Async) - (tryGetFileSource: string -> Async>) + (tryGetFileSource: string -> Async>) (tryGetProjectOptionsForFsproj: string -> Async) (getAllProjectOptions: unit -> Async) (includeDeclarations: bool) @@ -770,7 +769,7 @@ module Commands = (errorOnFailureToFixRange: bool) pos lineStr - (text: NamedText) + (text: IFSACSourceText) (tyRes: ParseAndCheckResults) : Async, Range[]>), string>> = asyncResult { @@ -779,7 +778,7 @@ module Commands = let symbolNameCore = symbol.DisplayNameCore - let tryAdjustRanges (text: NamedText, ranges: seq) = + let tryAdjustRanges (text: IFSACSourceText, ranges: seq) = let ranges = ranges |> Seq.map (fun range -> range.NormalizeDriveLetterCasing()) if errorOnFailureToFixRange then @@ -949,7 +948,7 @@ module Commands = /// /// Also does very basic validation of `newName`: /// * Must be valid operator name when operator - let adjustRenameSymbolNewName pos lineStr (text: NamedText) (tyRes: ParseAndCheckResults) (newName: string) = + let adjustRenameSymbolNewName pos lineStr (text: IFSACSourceText) (tyRes: ParseAndCheckResults) (newName: string) = asyncResult { let! symbolUse = tyRes.TryGetSymbolUse pos lineStr @@ -989,11 +988,11 @@ module Commands = /// Rename for Active Pattern Cases is disabled: /// `SymbolUseWorkspace` returns ranges for ALL Cases of that Active Pattern instead of just the single case let renameSymbolRange - (getDeclarationLocation: FSharpSymbolUse * NamedText -> Async) + (getDeclarationLocation: FSharpSymbolUse * IFSACSourceText -> Async) (includeBackticks: bool) pos lineStr - (text: NamedText) + (text: IFSACSourceText) (tyRes: ParseAndCheckResults) = asyncResult { @@ -1155,7 +1154,14 @@ module Commands = [||] -type Commands(checker: FSharpCompilerServiceChecker, state: State, hasAnalyzers: bool, rootPath: string option) = +type Commands + ( + checker: FSharpCompilerServiceChecker, + state: State, + hasAnalyzers: bool, + rootPath: string option, + sourceTextFactory: ISourceTextFactory + ) = let fileParsed = Event() let fileChecked = Event * int>() @@ -1249,7 +1255,7 @@ type Commands(checker: FSharpCompilerServiceChecker, state: State, hasAnalyzers: let res = Commands.analyzerHandler ( file, - fileData.Lines.ToString().Split("\n"), + fileData.Source.ToString().Split("\n"), parseAndCheck.GetParseResults.ParseTree, tast, parseAndCheck.GetCheckResults.PartialAssemblySignature.Entities |> Seq.toList, @@ -1337,14 +1343,14 @@ type Commands(checker: FSharpCompilerServiceChecker, state: State, hasAnalyzers: try let sourceOpt = match state.Files.TryFind file with - | Some f -> Some(f.Lines) + | Some f -> Some(f.Source) | None when File.Exists(UMX.untag file) -> let ctn = File.ReadAllText(UMX.untag file) - let text = NamedText(file, ctn) + let text = sourceTextFactory.Create(file, ctn) state.Files.[file] <- - { Touched = DateTime.Now - Lines = text + { LastTouched = DateTime.Now + Source = text Version = None } Some text @@ -1418,7 +1424,8 @@ type Commands(checker: FSharpCompilerServiceChecker, state: State, hasAnalyzers: member __.LastCheckResult = lastCheckResult - member __.SetFileContent(file: string, lines: NamedText, version) = state.AddFileText(file, lines, version) + member __.SetFileContent(file: string, lines: IFSACSourceText, version) = + state.AddFileText(file, lines, version) member private x.MapResultAsync ( @@ -1645,7 +1652,7 @@ type Commands(checker: FSharpCompilerServiceChecker, state: State, hasAnalyzers: /// Gets the current project options for the given file. /// If the file is a script, determines if the file content is changed enough to warrant new project options, /// and if so registers them. - member x.EnsureProjectOptionsForFile(file: string, text: NamedText, version, fsiRefs) = + member x.EnsureProjectOptionsForFile(file: string, text: IFSACSourceText, version, fsiRefs) = async { match state.GetProjectOptions(file) with | Some opts -> @@ -1756,7 +1763,7 @@ type Commands(checker: FSharpCompilerServiceChecker, state: State, hasAnalyzers: ( file: string, version: int, - content: NamedText, + content: IFSACSourceText, tfmConfig: FSIRefs.TFM, isFirstOpen: bool ) : Async = @@ -1805,7 +1812,13 @@ type Commands(checker: FSharpCompilerServiceChecker, state: State, hasAnalyzers: |> Async.Sequential |> Async.map ignore - member private x.CheckFile(file, text: NamedText, version: int, projectOptions: FSharpProjectOptions) : Async = + member private x.CheckFile + ( + file, + text: IFSACSourceText, + version: int, + projectOptions: FSharpProjectOptions + ) : Async = async { do x.CancelQueue file return! x.CheckCore(file, version, text, projectOptions) @@ -1875,7 +1888,7 @@ type Commands(checker: FSharpCompilerServiceChecker, state: State, hasAnalyzers: | None -> //Isn't in sync filled cache, we don't have result return CoreResponse.ErrorRes(sprintf "No help text available for symbol '%s'" sym) | Some(decl, pos, fn) -> //Is in sync filled cache, try to get results from async filled caches or calculate if it's not there - let source = state.Files.TryFind fn |> Option.map (fun n -> n.Lines) + let source = state.Files.TryFind fn |> Option.map (fun n -> n.Source) match source with | None -> return CoreResponse.ErrorRes(sprintf "No help text available for symbol '%s'" sym) @@ -1908,7 +1921,7 @@ type Commands(checker: FSharpCompilerServiceChecker, state: State, hasAnalyzers: (tyRes: ParseAndCheckResults) (pos: Position) lineStr - (lines: NamedText) + (lines: IFSACSourceText) (fileName: string) filter includeKeywords @@ -2171,7 +2184,7 @@ type Commands(checker: FSharpCompilerServiceChecker, state: State, hasAnalyzers: ( pos, lineStr, - text: NamedText, + text: IFSACSourceText, tyRes: ParseAndCheckResults, includeDeclarations: bool, includeBackticks: bool, @@ -2222,11 +2235,11 @@ type Commands(checker: FSharpCompilerServiceChecker, state: State, hasAnalyzers: tyRes } - member x.RenameSymbolRange(pos: Position, tyRes: ParseAndCheckResults, lineStr: LineStr, text: NamedText) = + member x.RenameSymbolRange(pos: Position, tyRes: ParseAndCheckResults, lineStr: LineStr, text: IFSACSourceText) = Commands.renameSymbolRange x.GetDeclarationLocation false pos lineStr text tyRes /// Also checks if rename is valid via `RenameSymbolRange` (-> `Error` -> invalid) - member x.RenameSymbol(pos: Position, tyRes: ParseAndCheckResults, lineStr: LineStr, text: NamedText) = + member x.RenameSymbol(pos: Position, tyRes: ParseAndCheckResults, lineStr: LineStr, text: IFSACSourceText) = asyncResult { // safety check: rename valid? let! _ = x.RenameSymbolRange(pos, tyRes, lineStr, text) @@ -2266,7 +2279,7 @@ type Commands(checker: FSharpCompilerServiceChecker, state: State, hasAnalyzers: ( tyRes: ParseAndCheckResults, pos: Position, - lines: NamedText, + lines: IFSACSourceText, triggerChar, possibleSessionKind ) = @@ -2317,7 +2330,7 @@ type Commands(checker: FSharpCompilerServiceChecker, state: State, hasAnalyzers: |> x.AsCancellable tyRes.FileName |> AsyncResult.recoverCancellation - member x.GetRecordStub (tyRes: ParseAndCheckResults) (pos: Position) (lines: NamedText) (line: LineStr) = + member x.GetRecordStub (tyRes: ParseAndCheckResults) (pos: Position) (lines: IFSACSourceText) (line: LineStr) = Commands.getRecordStub (tryFindRecordDefinitionFromPos codeGenServer) tyRes pos lines line |> x.AsCancellable tyRes.FileName @@ -2326,7 +2339,7 @@ type Commands(checker: FSharpCompilerServiceChecker, state: State, hasAnalyzers: member x.GetAbstractClassStub (tyRes: ParseAndCheckResults) (objExprRange: Range) - (lines: NamedText) + (lines: IFSACSourceText) (lineStr: LineStr) = let tryFindAbstractClassExprInBufferAtPos = @@ -2529,7 +2542,8 @@ type Commands(checker: FSharpCompilerServiceChecker, state: State, hasAnalyzers: FsAutoComplete.Core.InlayHints.provideHints (text, tyRes, range, hintConfig) - static member InlineValues(contents: NamedText, tyRes: ParseAndCheckResults) = Commands.inlineValues contents tyRes + static member InlineValues(contents: IFSACSourceText, tyRes: ParseAndCheckResults) = + Commands.inlineValues contents tyRes member __.PipelineHints(tyRes: ParseAndCheckResults) = Commands.pipelineHints (state.TryGetFileSource >> Async.singleton) tyRes diff --git a/src/FsAutoComplete.Core/FileSystem.fs b/src/FsAutoComplete.Core/FileSystem.fs index 215ac898f..22316b919 100644 --- a/src/FsAutoComplete.Core/FileSystem.fs +++ b/src/FsAutoComplete.Core/FileSystem.fs @@ -10,6 +10,21 @@ open FsToolkit.ErrorHandling open System.IO open FSharp.Compiler.IO +open System.Threading.Tasks +open IcedTasks + + +module File = + let getLastWriteTimeOrDefaultNow (path: string) = + let path = UMX.untag path + + if File.Exists path then + File.GetLastWriteTimeUtc path + else + DateTime.UtcNow + + let openFileStreamForReadingAsync (path: string) = + new FileStream((UMX.untag path), FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize = 4096, useAsync = true) [] module PositionExtensions = @@ -64,6 +79,71 @@ module RangeExtensions = member inline r.WithStart(start) = Range.mkRange r.FileName start r.End member inline r.WithEnd(fin) = Range.mkRange r.FileName r.Start fin + member inline range.ToRoslynTextSpan(sourceText: Microsoft.CodeAnalysis.Text.SourceText) = + + let startPosition = + sourceText.Lines.[max 0 (range.StartLine - 1)].Start + range.StartColumn + + let endPosition = + sourceText.Lines.[min (range.EndLine - 1) (sourceText.Lines.Count - 1)].Start + + range.EndColumn + + Microsoft.CodeAnalysis.Text.TextSpan(startPosition, endPosition - startPosition) + +/// A SourceText with operations commonly used in FsAutocomplete +type IFSACSourceText = + abstract member String: string + /// The local absolute path of the file whose contents this IFSACSourceText represents + abstract member FileName: string + /// The unwrapped local absolute path of the file whose contents this IFSACSourceText represents. + /// Should only be used when interoping with the Compiler/Serialization + abstract member RawFileName: string + /// Representation of the final position in this file + abstract member LastFilePosition: Position + /// Representation of the entire contents of the file, for inclusion checks + abstract member TotalRange: Range + /// Provides line-by-line access to the underlying text. + /// This can lead to unsafe access patterns, consider using one of the range or position-based + /// accessors instead + abstract member Lines: string array + /// Provides safe access to a substring of the file via FCS-provided Range + abstract member GetText: range: Range -> Result + /// Provides safe access to a line of the file via FCS-provided Position + abstract member GetLine: position: Position -> option + /// Provide safe access to the length of a line of the file via FCS-provided Position + abstract member GetLineLength: position: Position -> option + abstract member GetCharUnsafe: position: Position -> char + /// Provides safe access to a character of the file via FCS-provided Position. + /// Also available in indexer form: x[pos] + abstract member TryGetChar: position: Position -> option + /// Provides safe incrementing of a lien in the file via FCS-provided Position + abstract member NextLine: position: Position -> option + /// Provides safe incrementing of a position in the file via FCS-provided Position + abstract member NextPos: position: Position -> option + /// Provides safe incrementing of positions in a file while returning the character at the new position. + /// Intended use is for traversal loops. + abstract member TryGetNextChar: position: Position -> option + /// Provides safe decrementing of a position in the file via FCS-provided Position + abstract member PrevPos: position: Position -> option + /// Provides safe decrementing of positions in a file while returning the character at the new position. + /// Intended use is for traversal loops. + abstract member TryGetPrevChar: position: Position -> option + /// create a new IFSACSourceText for this file with the given text inserted at the given range. + abstract member ModifyText: range: Range * text: string -> Result + /// Safe access to the char in a file by Position + abstract Item: index: Position -> option with get + /// Safe access to the contents of a file by Range + abstract Item: index: Range -> Result with get + + abstract member WalkForward: + position: Position * terminal: (char -> bool) * condition: (char -> bool) -> option + + abstract member WalkBackwards: + position: Position * terminal: (char -> bool) * condition: (char -> bool) -> option + + inherit ISourceText + + /// A copy of the StringText type from F#.Compiler.Text, which is private. /// Adds a UOM-typed filename to make range manipulation easier, as well as /// safer traversals @@ -113,16 +193,16 @@ type NamedText(fileName: string, str: string) = override _.Equals(obj: obj) = match obj with - | :? NamedText as other -> other.String.Equals(str) + | :? IFSACSourceText as other -> other.String.Equals(str) | :? string as other -> other.Equals(str) | _ -> false override _.ToString() = str - /// The local absolute path of the file whose contents this NamedText represents + /// The local absolute path of the file whose contents this IFSACSourceText represents member x.FileName = fileName - /// The unwrapped local abolute path of the file whose contents this NamedText represents. + /// The unwrapped local abolute path of the file whose contents this IFSACSourceText represents. /// Should only be used when interoping with the Compiler/Serialization member x.RawFileName = UMX.untag fileName @@ -130,10 +210,8 @@ type NamedText(fileName: string, str: string) = member x.LastFilePosition = safeLastCharPos.Value /// Cached representation of the entire contents of the file, for inclusion checks - member x.TotalRange = totalRange.Value - /// Provides safe access to a substring of the file via FCS-provided Range member x.GetText(m: FSharp.Compiler.Text.Range) : Result = if not (Range.rangeContainsRange x.TotalRange m) then @@ -266,7 +344,7 @@ type NamedText(fileName: string, str: string) = let endRange = Range.mkRange x.RawFileName m.End x.TotalRange.End startRange, endRange - /// create a new NamedText for this file with the given text inserted at the given range. + /// create a new IFSACSourceText for this file with the given text inserted at the given range. member x.ModifyText(m: FSharp.Compiler.Text.Range, text: string) : Result = result { let startRange, endRange = x.SplitAt(m) @@ -354,83 +432,372 @@ type NamedText(fileName: string, str: string) = member this.ContentEquals(sourceText) = match sourceText with - | :? NamedText as sourceText when sourceText = this || sourceText.String = str -> true + | :? IFSACSourceText as sourceText when sourceText = this || sourceText.String = str -> true | _ -> false member _.CopyTo(sourceIndex, destination, destinationIndex, count) = str.CopyTo(sourceIndex, destination, destinationIndex, count) -type VolatileFile = - { Touched: DateTime - Lines: NamedText - Version: int option } + interface IFSACSourceText with + member x.String = x.String + member x.FileName = x.FileName + member x.RawFileName = x.RawFileName + member x.LastFilePosition = x.LastFilePosition + member x.TotalRange = x.TotalRange + member x.Lines = x.Lines + member x.GetText r = x.GetText r + member x.GetLine p = x.GetLine p + member x.GetLineLength i = x.GetLineLength i + member x.GetCharUnsafe p = x.GetCharUnsafe p + member x.TryGetChar p = x.TryGetChar p + member x.NextLine p = x.NextLine p + member x.NextPos p = x.NextPos p + member x.TryGetNextChar p = x.TryGetNextChar p + member x.PrevPos p = x.PrevPos p + member x.TryGetPrevChar p = x.TryGetPrevChar p + member x.ModifyText(r, t) = x.ModifyText(r, t) |> Result.map unbox + + member x.Item + with get (m: FSharp.Compiler.Text.Range) = x.Item m + + member x.Item + with get (pos: FSharp.Compiler.Text.Position) = x.Item pos + + member x.WalkForward(start, terminal, condition) = + x.WalkForward(start, terminal, condition) + + member x.WalkBackwards(start, terminal, condition) = + x.WalkBackwards(start, terminal, condition) + +module RoslynSourceText = + open Microsoft.CodeAnalysis.Text + + /// Ported from Roslyn.Utilities + [] + module Hash = + /// (From Roslyn) This is how VB Anonymous Types combine hash values for fields. + let combine (newKey: int) (currentKey: int) = + (currentKey * (int 0xA5555529)) + newKey + + let combineValues (values: seq<'T>) = + (0, values) ||> Seq.fold (fun hash value -> combine (value.GetHashCode()) hash) + + let weakTable = ConditionalWeakTable() + + let rec create (fileName: string, sourceText: SourceText) : IFSACSourceText = + + let walk + ( + x: IFSACSourceText, + start: FSharp.Compiler.Text.Position, + (posChange: FSharp.Compiler.Text.Position -> FSharp.Compiler.Text.Position option), + terminal, + condition + ) = + /// if the condition is never met, return None + + let firstPos = Position.pos0 + let finalPos = x.LastFilePosition + + let rec loop (pos: FSharp.Compiler.Text.Position) : FSharp.Compiler.Text.Position option = + option { + let! charAt = x[pos] + do! Option.guard (firstPos <> pos && finalPos <> pos) + do! Option.guard (not (terminal charAt)) + + if condition charAt then + return pos + else + let! nextPos = posChange pos + return! loop nextPos + } - member this.FileName = this.Lines.FileName + loop start - /// Updates the Lines value - member this.SetLines(lines) = { this with Lines = lines } - /// Updates the Lines value with supplied text - member this.SetText(text) = - this.SetLines(NamedText(this.Lines.FileName, text)) + let inline totalLinesLength () = sourceText.Lines |> Seq.length + let sourceText = + { - /// Updates the Touched value - member this.SetTouched touched = { this with Touched = touched } + new Object() with + override _.ToString() = sourceText.ToString() + override _.GetHashCode() = + let checksum = sourceText.GetChecksum() - /// Updates the Touched value attempting to use the file on disk's GetLastWriteTimeUtc otherwise uses DateTime.UtcNow. - member this.UpdateTouched() = - let path = UMX.untag this.Lines.FileName + let contentsHash = + if not checksum.IsDefault then + Hash.combineValues checksum + else + 0 + + let encodingHash = + if not (isNull sourceText.Encoding) then + sourceText.Encoding.GetHashCode() + else + 0 + + sourceText.ChecksumAlgorithm.GetHashCode() + |> Hash.combine encodingHash + |> Hash.combine contentsHash + |> Hash.combine sourceText.Length + interface IFSACSourceText with + + member x.Item + with get (index: Range): Result = x.GetText(index) + + member x.Item + with get (index: Position): char option = x.TryGetChar(index) + + member x.WalkBackwards(start: Position, terminal: char -> bool, condition: char -> bool) : Position option = + walk (x, start, x.PrevPos, terminal, condition) + + member x.WalkForward(start: Position, terminal: char -> bool, condition: char -> bool) : Position option = + walk (x, start, x.NextPos, terminal, condition) + + member x.String: string = sourceText.ToString() + member x.FileName: string = fileName + member x.RawFileName: string = UMX.untag fileName + + member x.LastFilePosition: Position = + let endLine, endChar = (x :> ISourceText).GetLastCharacterPosition() + Position.mkPos endLine endChar + + member x.TotalRange: Range = + (Range.mkRange (UMX.untag fileName) Position.pos0 (x.LastFilePosition)) + + member x.Lines: string array = + sourceText.Lines |> Seq.toArray |> Array.map (fun l -> l.ToString()) - let dt = - if File.Exists path then - File.GetLastWriteTimeUtc path - else - DateTime.UtcNow + member this.GetText(range: Range) : Result = + range.ToRoslynTextSpan(sourceText) |> sourceText.GetSubText |> string |> Ok + + member x.GetLine(pos: Position) : string option = + if pos.Line < 1 || pos.Line > totalLinesLength () then + None + else + Some((x :> ISourceText).GetLineString(pos.Line - 1)) + + member x.GetLineLength(pos: Position) : int option = + if pos.Line > totalLinesLength () then + None + else + Some((x :> ISourceText).GetLineString(pos.Line - 1).Length) + + member x.GetCharUnsafe(pos: Position) : char = x.GetLine(pos).Value[pos.Column - 1] + + member x.TryGetChar(pos: Position) : char option = + option { + do! Option.guard (Range.rangeContainsPos (x.TotalRange) pos) - this.SetTouched dt + if pos.Column = 0 then + return! None + else + let lineIndex = pos.Column - 1 + let! lineText = x.GetLine(pos) + if lineText.Length <= lineIndex then + return! None + else + return lineText[lineIndex] + } + + member this.NextLine(pos: Position) : Position option = + if pos.Line < totalLinesLength () then + Position.mkPos (pos.Line + 1) 0 |> Some + else + None - /// Helper method to create a VolatileFile - static member Create(lines, version, touched) = - { Lines = lines - Version = version - Touched = touched } + member x.NextPos(pos: Position) : Position option = + option { + let! currentLine = x.GetLine pos + + if pos.Column - 1 = currentLine.Length then + if totalLinesLength () > pos.Line then + // advance to the beginning of the next line + return Position.mkPos (pos.Line + 1) 0 + else + return! None + else + return Position.mkPos pos.Line (pos.Column + 1) + } + + member x.TryGetNextChar(pos: Position) : (Position * char) option = + option { + let! np = x.NextPos pos + return np, x.GetCharUnsafe np + } + + member x.PrevPos(pos: Position) : Position option = + option { + if pos.Column <> 0 then + return Position.mkPos pos.Line (pos.Column - 1) + else if pos.Line <= 1 then + return! None + else if totalLinesLength () > pos.Line - 2 then + let prevLine = (x :> ISourceText).GetLineString(pos.Line - 2) + // retreat to the end of the previous line + return Position.mkPos (pos.Line - 1) (prevLine.Length - 1) + else + return! None + } + + member x.TryGetPrevChar(pos: FSharp.Compiler.Text.Position) : (FSharp.Compiler.Text.Position * char) option = + option { + let! np = x.PrevPos pos + let! prevLineLength = x.GetLineLength(np) + + if np.Column < 1 || prevLineLength < np.Column then + return! x.TryGetPrevChar(np) + else + return np, x.GetCharUnsafe np + } + + member x.ModifyText(range: Range, text: string) : Result = + let span = range.ToRoslynTextSpan(sourceText) + let change = TextChange(span, text) + Ok(create (fileName, sourceText.WithChanges(change))) + + + + interface ISourceText with + + member _.Item + with get index = sourceText.[index] + + member _.GetLineString(lineIndex) = sourceText.Lines.[lineIndex].ToString() + + member _.GetLineCount() = sourceText.Lines.Count + + member _.GetLastCharacterPosition() = + if sourceText.Lines.Count > 0 then + (sourceText.Lines.Count, sourceText.Lines.[sourceText.Lines.Count - 1].Span.Length) + else + (0, 0) + + member _.GetSubTextString(start, length) = + sourceText.GetSubText(TextSpan(start, length)).ToString() + + member _.SubTextEquals(target, startIndex) = + if startIndex < 0 || startIndex >= sourceText.Length then + invalidArg "startIndex" "Out of range." + + if String.IsNullOrEmpty(target) then + invalidArg "target" "Is null or empty." + + let lastIndex = startIndex + target.Length + + if lastIndex <= startIndex || lastIndex >= sourceText.Length then + invalidArg "target" "Too big." + + let mutable finished = false + let mutable didEqual = true + let mutable i = 0 + + while not finished && i < target.Length do + if target.[i] <> sourceText.[startIndex + i] then + didEqual <- false + finished <- true // bail out early + else + i <- i + 1 + + didEqual + + member _.ContentEquals(sourceText) = + match sourceText with + | :? SourceText as sourceText -> sourceText.ContentEquals(sourceText) + | _ -> false - /// Helper method to create a VolatileFile - static member Create(path, text, version, touched) = - VolatileFile.Create(NamedText(path, text), version, touched) + member _.Length = sourceText.Length + + member _.CopyTo(sourceIndex, destination, destinationIndex, count) = + sourceText.CopyTo(sourceIndex, destination, destinationIndex, count) - /// Helper method to create a VolatileFile, attempting to use the file on disk's GetLastWriteTimeUtc otherwise uses DateTime.UtcNow. - static member Create(path: string, text, version) = - let touched = - let path = UMX.untag path + } - if File.Exists path then - File.GetLastWriteTimeUtc path - else - DateTime.UtcNow + sourceText + +type ISourceTextFactory = + abstract member Create: fileName: string * text: string -> IFSACSourceText + abstract member Create: fileName: string * stream: Stream -> CancellableValueTask + +type NamedTextFactory() = + interface ISourceTextFactory with + member this.Create(fileName: string, text: string) : IFSACSourceText = NamedText(fileName, text) + + member this.Create(fileName: string, stream: Stream) : CancellableValueTask = + cancellableValueTask { + use reader = new StreamReader(stream) +#if NET6_0 + let! text = reader.ReadToEndAsync() +#else + let! text = fun ct -> reader.ReadToEndAsync(ct) +#endif + return NamedText(fileName, text) :> IFSACSourceText + } + +type RoslynSourceTextFactory() = + interface ISourceTextFactory with + member this.Create(fileName: string, text: string) : IFSACSourceText = + // This uses a TextReader because the TextReader overload https://github.com/dotnet/roslyn/blob/6df76ec8b109c9460f7abccc3a310c7cdbd2975e/src/Compilers/Core/Portable/Text/SourceText.cs#L120-L139 + // attempts to use the LargeText implementation for large strings. While the string is already allocated, if using CONSERVE_MEMORY, it should be cleaned up and compacted eventually. + use t = new StringReader(text) + RoslynSourceText.create (fileName, (Microsoft.CodeAnalysis.Text.SourceText.From(t, text.Length))) - VolatileFile.Create(path, text, version, touched) + member this.Create(fileName: string, stream: Stream) : CancellableValueTask = + fun ct -> + ct.ThrowIfCancellationRequested() + // Maybe one day we'll have an async version for streams: https://github.com/dotnet/roslyn/issues/61489 + RoslynSourceText.create (fileName, (Microsoft.CodeAnalysis.Text.SourceText.From(stream))) + |> ValueTask.FromResult + + +type VolatileFile = + { LastTouched: DateTime + Source: IFSACSourceText + Version: int option } + member this.FileName = this.Source.FileName + + /// Updates the Source value + member this.SetSource(source) = { this with Source = source } + + /// Updates the Touched value + member this.SetLastTouched touched = { this with LastTouched = touched } + + /// Updates the Touched value attempting to use the file on disk's GetLastWriteTimeUtc otherwise uses DateTime.UtcNow. + member this.UpdateTouched() = + let dt = File.getLastWriteTimeOrDefaultNow this.Source.FileName + this.SetLastTouched dt + + + /// Helper method to create a VolatileFile + static member Create(source: IFSACSourceText, ?version: int, ?touched: DateTime) = + let touched = + match touched with + | Some t -> t + | None -> File.getLastWriteTimeOrDefaultNow source.FileName + + { Source = source + Version = version + LastTouched = touched } type FileSystem(actualFs: IFileSystem, tryFindFile: string -> VolatileFile option) = let fsLogger = LogProvider.getLoggerByName "FileSystem" let getContent (filename: string) = - filename |> tryFindFile |> Option.map (fun file -> fsLogger.debug ( Log.setMessage "Getting content of `{path}` - {hash}" >> Log.addContext "path" filename - >> Log.addContext "hash" (file.Lines.GetHashCode()) + >> Log.addContext "hash" (file.Source.GetHashCode()) ) - file.Lines.ToString() |> System.Text.Encoding.UTF8.GetBytes) + file.Source.ToString() |> System.Text.Encoding.UTF8.GetBytes) /// translation of the BCL's Windows logic for Path.IsPathRooted. /// @@ -479,7 +846,7 @@ type FileSystem(actualFs: IFileSystem, tryFindFile: string -> Volatil filename |> Utils.normalizePath |> tryFindFile - |> Option.map (fun f -> f.Touched) + |> Option.map (fun f -> f.LastTouched) |> Option.defaultWith (fun () -> actualFs.GetLastWriteTimeShim filename) // fsLogger.debug ( @@ -491,7 +858,6 @@ type FileSystem(actualFs: IFileSystem, tryFindFile: string -> Volatil result member _.NormalizePathShim(f: string) = f |> Utils.normalizePath |> UMX.untag - member _.IsInvalidPathShim(f) = actualFs.IsInvalidPathShim f member _.GetTempPathShim() = actualFs.GetTempPathShim() member _.IsStableFileHeuristic(f) = actualFs.IsStableFileHeuristic f @@ -567,7 +933,12 @@ module Tokenizer = /// /// /// based on: `dotnet/fsharp` `Tokenizer.fixupSpan` - let private tryFixupRangeBySplittingAtDot (range: Range, text: NamedText, includeBackticks: bool) : Range voption = + let private tryFixupRangeBySplittingAtDot + ( + range: Range, + text: IFSACSourceText, + includeBackticks: bool + ) : Range voption = match text[range] with | Error _ -> ValueNone | Ok rangeText when rangeText.EndsWith "``" -> @@ -628,7 +999,13 @@ module Tokenizer = /// -> full identifier range with backticks, just identifier name (~`symbolNameCore`) without backticks /// /// returns `None` iff `range` isn't inside `text` -> `range` & `text` for different states - let tryFixupRange (symbolNameCore: string, range: Range, text: NamedText, includeBackticks: bool) : Range voption = + let tryFixupRange + ( + symbolNameCore: string, + range: Range, + text: IFSACSourceText, + includeBackticks: bool + ) : Range voption = // first: try match symbolNameCore in last line // usually identifier cannot contain linebreak -> is in last line of range // Exception: Active Pattern can span multiple lines: `(|Even|Odd|)` -> `(|Even|\n Odd|)` is valid too diff --git a/src/FsAutoComplete.Core/InlayHints.fs b/src/FsAutoComplete.Core/InlayHints.fs index 99dfc77cd..e329bcdd2 100644 --- a/src/FsAutoComplete.Core/InlayHints.fs +++ b/src/FsAutoComplete.Core/InlayHints.fs @@ -552,7 +552,7 @@ let rec private getParensForPatternWithIdent (patternRange: Range) (identStart: /// not `accessibility`. /// /// Note: doesn't handle when accessibility is on prev line -let private rangeOfNamedPat (text: NamedText) (pat: SynPat) = +let private rangeOfNamedPat (text: IFSACSourceText) (pat: SynPat) = match pat with | SynPat.Named(accessibility = None) -> pat.Range | SynPat.Named(ident = SynIdent(ident = ident); accessibility = Some(access)) -> @@ -585,7 +585,7 @@ let private rangeOfNamedPat (text: NamedText) (pat: SynPat) = | _ -> failwith "Pattern must be Named!" /// Note: (deliberately) fails when `pat` is neither `Named` nor `OptionalVal` -let rec private getParensForIdentPat (text: NamedText) (pat: SynPat) (path: SyntaxVisitorPath) = +let rec private getParensForIdentPat (text: IFSACSourceText) (pat: SynPat) (path: SyntaxVisitorPath) = match pat with | SynPat.Named(ident = SynIdent(ident = ident)) -> // neither `range`, not `pat.Range` includes `accessibility`... @@ -599,7 +599,7 @@ let rec private getParensForIdentPat (text: NamedText) (pat: SynPat) (path: Synt getParensForPatternWithIdent patternRange identStart path | _ -> failwith "Pattern must be Named or OptionalVal!" -let tryGetExplicitTypeInfo (text: NamedText, ast: ParsedInput) (pos: Position) : ExplicitType option = +let tryGetExplicitTypeInfo (text: IFSACSourceText, ast: ParsedInput) (pos: Position) : ExplicitType option = SyntaxTraversal.Traverse( pos, ast, @@ -858,7 +858,7 @@ let isPotentialTargetForTypeAnnotation let tryGetDetailedExplicitTypeInfo (isValidTarget: FSharpSymbolUse * FSharpMemberOrFunctionOrValue -> bool) - (text: NamedText, parseAndCheck: ParseAndCheckResults) + (text: IFSACSourceText, parseAndCheck: ParseAndCheckResults) (pos: Position) = maybe { @@ -896,7 +896,13 @@ type HintConfig = { ShowTypeHints: bool ShowParameterHints: bool } -let provideHints (text: NamedText, parseAndCheck: ParseAndCheckResults, range: Range, hintConfig) : Async = +let provideHints + ( + text: IFSACSourceText, + parseAndCheck: ParseAndCheckResults, + range: Range, + hintConfig + ) : Async = asyncResult { let! cancellationToken = Async.CancellationToken diff --git a/src/FsAutoComplete.Core/SignatureHelp.fs b/src/FsAutoComplete.Core/SignatureHelp.fs index b42a56ca2..2398f0716 100644 --- a/src/FsAutoComplete.Core/SignatureHelp.fs +++ b/src/FsAutoComplete.Core/SignatureHelp.fs @@ -31,7 +31,7 @@ let private getSignatureHelpForFunctionApplication tyRes: ParseAndCheckResults, caretPos: Position, endOfPreviousIdentPos: Position, - lines: NamedText + lines: IFSACSourceText ) : Async = asyncMaybe { let! lineStr = lines.GetLine endOfPreviousIdentPos @@ -123,7 +123,13 @@ let private getSignatureHelpForFunctionApplication | _ -> return! None } -let private getSignatureHelpForMethod (tyRes: ParseAndCheckResults, caretPos: Position, lines: NamedText, triggerChar) = +let private getSignatureHelpForMethod + ( + tyRes: ParseAndCheckResults, + caretPos: Position, + lines: IFSACSourceText, + triggerChar + ) = asyncMaybe { let! paramLocations = tyRes.GetParseResults.FindParameterLocations caretPos let names = paramLocations.LongId @@ -206,7 +212,7 @@ let getSignatureHelpFor ( tyRes: ParseAndCheckResults, pos: Position, - lines: NamedText, + lines: IFSACSourceText, triggerChar, possibleSessionKind ) = diff --git a/src/FsAutoComplete.Core/State.fs b/src/FsAutoComplete.Core/State.fs index 4209f4f95..ba99ea8a1 100644 --- a/src/FsAutoComplete.Core/State.fs +++ b/src/FsAutoComplete.Core/State.fs @@ -182,12 +182,12 @@ type State = ColorizationOutput = false WorkspaceStateDirectory = workspaceStateDir } - member x.RefreshCheckerOptions(file: string, text: NamedText) : FSharpProjectOptions option = + member x.RefreshCheckerOptions(file: string, text: IFSACSourceText) : FSharpProjectOptions option = x.ProjectController.GetProjectOptions(UMX.untag file) |> Option.map (fun opts -> x.Files.[file] <- - { Lines = text - Touched = DateTime.Now + { Source = text + LastTouched = DateTime.Now Version = None } opts) @@ -216,19 +216,19 @@ type State = member x.SetLastCheckedVersion (file: string) (version: int) = x.LastCheckedVersion.[file] <- version - member x.AddFileTextAndCheckerOptions(file: string, text: NamedText, opts, version) = + member x.AddFileTextAndCheckerOptions(file: string, text: IFSACSourceText, opts, version) = let fileState = - { Lines = text - Touched = DateTime.Now + { Source = text + LastTouched = DateTime.Now Version = version } x.Files.[file] <- fileState x.ProjectController.SetProjectOptions(UMX.untag file, opts) - member x.AddFileText(file: string, text: NamedText, version) = + member x.AddFileText(file: string, text: IFSACSourceText, version) = let fileState = - { Lines = text - Touched = DateTime.Now + { Source = text + LastTouched = DateTime.Now Version = version } x.Files.[file] <- fileState @@ -259,32 +259,32 @@ type State = member x.TryGetFileCheckerOptionsWithLines (file: string) - : ResultOrString = + : ResultOrString = match x.Files.TryFind(file) with | None -> ResultOrString.Error(sprintf "File '%s' not parsed" (UMX.untag file)) | Some(volFile) -> match x.ProjectController.GetProjectOptions((UMX.untag file)) with - | None -> Ok(State.FileWithoutProjectOptions(file), volFile.Lines) - | Some opts -> Ok(opts, volFile.Lines) + | None -> Ok(State.FileWithoutProjectOptions(file), volFile.Source) + | Some opts -> Ok(opts, volFile.Source) member x.TryGetFileCheckerOptionsWithSource (file: string) - : ResultOrString = + : ResultOrString = match x.TryGetFileCheckerOptionsWithLines(file) with | ResultOrString.Error x -> ResultOrString.Error x | Ok(opts, lines) -> Ok(opts, lines) - member x.TryGetFileSource(file: string) : ResultOrString = + member x.TryGetFileSource(file: string) : ResultOrString = match x.Files.TryFind(file) with | None -> ResultOrString.Error(sprintf "File '%s' not parsed" (UMX.untag file)) - | Some f -> Ok f.Lines + | Some f -> Ok f.Source member x.TryGetFileCheckerOptionsWithLinesAndLineStr ( file: string, pos: Position - ) : ResultOrString = + ) : ResultOrString = result { let! (opts, text) = x.TryGetFileCheckerOptionsWithLines(file) diff --git a/src/FsAutoComplete.Core/SymbolLocation.fs b/src/FsAutoComplete.Core/SymbolLocation.fs index b6f9701bc..8b6e33ce5 100644 --- a/src/FsAutoComplete.Core/SymbolLocation.fs +++ b/src/FsAutoComplete.Core/SymbolLocation.fs @@ -14,7 +14,7 @@ type SymbolDeclarationLocation = let getDeclarationLocation ( symbolUse: FSharpSymbolUse, - currentDocument: NamedText, + currentDocument: IFSACSourceText, getProjectOptions, projectsThatContainFile: string -> Async, getDependentProjectsOfProjects diff --git a/src/FsAutoComplete.Core/paket.references b/src/FsAutoComplete.Core/paket.references index f05099119..64e2b0548 100644 --- a/src/FsAutoComplete.Core/paket.references +++ b/src/FsAutoComplete.Core/paket.references @@ -16,3 +16,4 @@ Microsoft.Build.Utilities.Core Ionide.LanguageServerProtocol Ionide.KeepAChangelog.Tasks Microsoft.Extensions.Caching.Memory +Microsoft.CodeAnalysis diff --git a/src/FsAutoComplete/CodeFixes.fs b/src/FsAutoComplete/CodeFixes.fs index 05026d058..444501a2e 100644 --- a/src/FsAutoComplete/CodeFixes.fs +++ b/src/FsAutoComplete/CodeFixes.fs @@ -21,13 +21,13 @@ module Types = type IsEnabled = unit -> bool type GetRangeText = string -> LspTypes.Range -> Async> - type GetFileLines = string -> Async> - type GetLineText = NamedText -> LspTypes.Range -> Async> + type GetFileLines = string -> Async> + type GetLineText = IFSACSourceText -> LspTypes.Range -> Async> type GetParseResultsForFile = string -> FSharp.Compiler.Text.Position - -> Async> + -> Async> type GetProjectOptionsForFile = string -> Async> @@ -197,10 +197,10 @@ module Navigation = fcsPos - let inc (lines: NamedText) (pos: LspTypes.Position) : LspTypes.Position option = + let inc (lines: IFSACSourceText) (pos: LspTypes.Position) : LspTypes.Position option = lines.NextPos(protocolPosToPos pos) |> Option.map fcsPosToLsp - let dec (lines: NamedText) (pos: LspTypes.Position) : LspTypes.Position option = + let dec (lines: IFSACSourceText) (pos: LspTypes.Position) : LspTypes.Position option = lines.PrevPos(protocolPosToPos pos) |> Option.map fcsPosToLsp let rec decMany lines pos count = @@ -229,14 +229,15 @@ module Navigation = return pos } - let walkBackUntilConditionWithTerminal (lines: NamedText) pos condition terminal = + let walkBackUntilConditionWithTerminal (lines: IFSACSourceText) pos condition terminal = let fcsStartPos = protocolPosToPos pos lines.WalkBackwards(fcsStartPos, terminal, condition) |> Option.map fcsPosToLsp - let walkForwardUntilConditionWithTerminal (lines: NamedText) pos condition terminal = + let walkForwardUntilConditionWithTerminal (lines: IFSACSourceText) pos condition terminal = let fcsStartPos = protocolPosToPos pos + lines.WalkForward(fcsStartPos, terminal, condition) |> Option.map fcsPosToLsp let walkBackUntilCondition lines pos condition = diff --git a/src/FsAutoComplete/CodeFixes/AddPrivateAccessModifier.fs b/src/FsAutoComplete/CodeFixes/AddPrivateAccessModifier.fs index 0e4d47850..0dd625a12 100644 --- a/src/FsAutoComplete/CodeFixes/AddPrivateAccessModifier.fs +++ b/src/FsAutoComplete/CodeFixes/AddPrivateAccessModifier.fs @@ -17,7 +17,7 @@ type SymbolUseWorkspace = -> bool -> FSharp.Compiler.Text.Position -> LineStr - -> NamedText + -> IFSACSourceText -> ParseAndCheckResults -> Async, FSharp.Compiler.Text.range array>, string>> diff --git a/src/FsAutoComplete/CodeFixes/ConvertPositionalDUToNamed.fs b/src/FsAutoComplete/CodeFixes/ConvertPositionalDUToNamed.fs index 46f509912..5239f34c1 100644 --- a/src/FsAutoComplete/CodeFixes/ConvertPositionalDUToNamed.fs +++ b/src/FsAutoComplete/CodeFixes/ConvertPositionalDUToNamed.fs @@ -111,7 +111,7 @@ let private createWildCard endRange (duField: string) : TextEdit = let range = endRange { NewText = wildcard; Range = range } -let private toPosSeq (range: FSharp.Compiler.Text.Range, text: NamedText) = +let private toPosSeq (range: FSharp.Compiler.Text.Range, text: IFSACSourceText) = range.Start |> Seq.unfold (fun currentPos -> match text.NextPos currentPos with diff --git a/src/FsAutoComplete/CodeFixes/ConvertTripleSlashCommentToXmlTaggedDoc.fs b/src/FsAutoComplete/CodeFixes/ConvertTripleSlashCommentToXmlTaggedDoc.fs index ba46ceb52..31076b723 100644 --- a/src/FsAutoComplete/CodeFixes/ConvertTripleSlashCommentToXmlTaggedDoc.fs +++ b/src/FsAutoComplete/CodeFixes/ConvertTripleSlashCommentToXmlTaggedDoc.fs @@ -123,7 +123,7 @@ let private isAstElemWithPreXmlDoc input pos = let private collectCommentContents (startPos: FSharp.Compiler.Text.Position) (endPos: FSharp.Compiler.Text.Position) - (sourceText: NamedText) + (sourceText: IFSACSourceText) = let rec loop (p: FSharp.Compiler.Text.Position) acc = if p.Line > endPos.Line then diff --git a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs index 2dd7b9821..a60d16140 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs @@ -41,6 +41,7 @@ open FsAutoComplete.UnionPatternMatchCaseGenerator open System.Collections.Concurrent open System.Diagnostics open System.Text.RegularExpressions +open IcedTasks [] type WorkspaceChosen = @@ -56,7 +57,8 @@ type AdaptiveWorkspaceChosen = | Projs of amap, DateTime> | NotChosen -type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FSharpLspClient) = +type AdaptiveFSharpLspServer + (workspaceLoader: IWorkspaceLoader, lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFactory) = let logger = LogProvider.getLoggerFor () @@ -253,7 +255,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar let builtInCompilerAnalyzers config (file: VolatileFile) (tyRes: ParseAndCheckResults) = let filePath = file.FileName let filePathUntag = UMX.untag filePath - let source = file.Lines + let source = file.Source let version = file.Version @@ -269,7 +271,6 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar let! unused = UnusedOpens.getUnusedOpens (tyRes.GetCheckResults, getSourceLine) let! ct = Async.CancellationToken - notifications.Trigger(NotificationEvent.UnusedOpens(filePath, (unused |> List.toArray)), ct) with e -> logger.error (Log.setMessage "checkUnusedOpens failed" >> Log.addExn e) @@ -357,7 +358,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar let res = Commands.analyzerHandler ( file, - volatileFile.Lines.ToString().Split("\n"), + volatileFile.Source.ToString().Split("\n"), parseAndCheck.GetParseResults.ParseTree, tast, parseAndCheck.GetCheckResults.PartialAssemblySignature.Entities |> Seq.toList, @@ -864,7 +865,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar logger.debug ( Log.setMessage "TextChanged for file : {fileName} {touched} {version}" >> Log.addContextDestructured "fileName" v.FileName - >> Log.addContextDestructured "touched" v.Touched + >> Log.addContextDestructured "touched" v.LastTouched >> Log.addContextDestructured "version" v.Version ) @@ -872,11 +873,11 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar openFilesReadOnly |> AMap.map (fun filePath file -> aval { - let! (file) = file + let! file = file and! changes = textChangesReadOnly |> AMap.tryFind filePath match changes with - | None -> return (file) + | None -> return file | Some c -> let! ps = c |> ASet.toAVal @@ -892,15 +893,14 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar ||> Seq.fold (fun text (change, version, touched) -> match change.Range with | None -> // replace entire content - // We want to update the DateTime here since TextDocumentDidChange will not have changes reflected on disk - VolatileFile.Create(filePath, change.Text, Some version, touched) + VolatileFile.Create(sourceTextFactory.Create(filePath, change.Text), version, touched) | Some rangeToReplace -> // replace just this slice let fcsRangeToReplace = protocolRangeToRange (UMX.untag filePath) rangeToReplace try - match text.Lines.ModifyText(fcsRangeToReplace, change.Text) with - | Ok text -> VolatileFile.Create(text, Some version, touched) + match text.Source.ModifyText(fcsRangeToReplace, change.Text) with + | Ok text -> VolatileFile.Create(text, version, touched) | Error message -> logger.error ( @@ -948,6 +948,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar // ignore if already cancelled () + let cachedFileContents = cmap, asyncaval> () let resetCancellationToken filePath = let adder _ = new CancellationTokenSource() @@ -966,7 +967,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar let updater _ (v: cval<_>) = v.Value <- file resetCancellationToken file.FileName - transact (fun () -> openFiles.AddOrElse(file.Lines.FileName, adder, updater)) + transact (fun () -> openFiles.AddOrElse(file.Source.FileName, adder, updater)) let updateTextchanges filePath p = let adder _ = cset<_> [ p ] @@ -997,20 +998,15 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar >> Log.addContextDestructured "file" file ) - let untagged = UMX.untag file + use s = File.openFileStreamForReadingAsync file - if File.Exists untagged && isFileWithFSharp untagged then - let! change = File.ReadAllTextAsync untagged |> Async.AwaitTask - let lastWriteTime = File.GetLastWriteTimeUtc untagged + let! source = sourceTextFactory.Create(file, s) |> Async.AwaitCancellableValueTask - let file = - { Touched = lastWriteTime - Lines = NamedText(file, change) - Version = None } + return + { LastTouched = File.getLastWriteTimeOrDefaultNow file + Source = source + Version = None } - return file - else - return! None with e -> logger.warn ( Log.setMessage "Could not read file {file}" @@ -1024,11 +1020,13 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar do let fileshimChanges = openFilesWithChanges |> AMap.mapA (fun _ v -> v) + // let cachedFileContents = cachedFileContents |> cmap.mapA (fun _ v -> v) let filesystemShim file = // GetLastWriteTimeShim gets called _alot_ and when we do checks on save we use Async.Parallel for type checking. // Adaptive uses lots of locks under the covers, so many threads can get blocked waiting for data. // flattening openFilesWithChanges makes this check a lot quicker as it's not needing to recalculate each value. + fileshimChanges |> AMap.force |> HashMap.tryFind file FSharp.Compiler.IO.FileSystemAutoOpens.FileSystem <- @@ -1042,7 +1040,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar /// let parseFile (checker: FSharpCompilerServiceChecker) (source: VolatileFile) parseOpts options = async { - let! result = checker.ParseFile(source.FileName, source.Lines, parseOpts) + let! result = checker.ParseFile(source.FileName, source.Source, parseOpts) let! ct = Async.CancellationToken fileParsed.Trigger(result, options, ct) @@ -1074,7 +1072,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar } let forceFindSourceText filePath = - forceFindOpenFileOrRead filePath |> AsyncResult.map (fun f -> f.Lines) + forceFindOpenFileOrRead filePath |> AsyncResult.map (fun f -> f.Source) let openFilesToChangesAndProjectOptions = @@ -1090,7 +1088,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar let! cts = tryGetOpenFileToken filePath let! opts = - checker.GetProjectOptionsFromScript(filePath, file.Lines, tfmConfig) + checker.GetProjectOptionsFromScript(filePath, file.Source, tfmConfig) |> Async.withCancellation cts.Token |> Async.startImmediateAsTask ctok @@ -1166,7 +1164,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar let parseAndCheckFile (checker: FSharpCompilerServiceChecker) (file: VolatileFile) options shouldCache = async { let tags = - [ SemanticConventions.fsac_sourceCodePath, box (UMX.untag file.Lines.FileName) + [ SemanticConventions.fsac_sourceCodePath, box (UMX.untag file.Source.FileName) SemanticConventions.projectFilePath, box (options.ProjectFileName) ] use _ = fsacActivitySource.StartActivityForType(thisType, tags = tags) @@ -1174,31 +1172,31 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar logger.info ( Log.setMessage "Getting typecheck results for {file} - {hash} - {date}" - >> Log.addContextDestructured "file" file.Lines.FileName - >> Log.addContextDestructured "hash" (file.Lines.GetHashCode()) - >> Log.addContextDestructured "date" (file.Touched) + >> Log.addContextDestructured "file" file.Source.FileName + >> Log.addContextDestructured "hash" (file.Source.GetHashCode()) + >> Log.addContextDestructured "date" (file.LastTouched) ) let! ct = Async.CancellationToken use progressReport = new ServerProgressReport(lspClient) - let simpleName = Path.GetFileName(UMX.untag file.Lines.FileName) - do! progressReport.Begin($"Typechecking {simpleName}", message = $"{file.Lines.FileName}") + let simpleName = Path.GetFileName(UMX.untag file.Source.FileName) + do! progressReport.Begin($"Typechecking {simpleName}", message = $"{file.Source.FileName}") let! result = checker.ParseAndCheckFileInProject( - file.Lines.FileName, - (file.Lines.GetHashCode()), - file.Lines, + file.Source.FileName, + (file.Source.GetHashCode()), + file.Source, options, shouldCache = shouldCache ) - |> Debug.measureAsync $"checker.ParseAndCheckFileInProject - {file.Lines.FileName}" + |> Debug.measureAsync $"checker.ParseAndCheckFileInProject - {file.Source.FileName}" - do! progressReport.End($"Typechecked {file.Lines.FileName}") + do! progressReport.End($"Typechecked {file.Source.FileName}") - notifications.Trigger(NotificationEvent.FileParsed(file.Lines.FileName), ct) + notifications.Trigger(NotificationEvent.FileParsed(file.Source.FileName), ct) match result with | Error e -> @@ -1212,7 +1210,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar | Ok parseAndCheck -> logger.info ( Log.setMessage "Typecheck completed successfully for {file}" - >> Log.addContextDestructured "file" file.Lines.FileName + >> Log.addContextDestructured "file" file.Source.FileName ) Async.Start( @@ -1228,7 +1226,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar |> Array.distinctBy (fun e -> e.Severity, e.ErrorNumber, e.StartLine, e.StartColumn, e.EndLine, e.EndColumn, e.Message) - notifications.Trigger(NotificationEvent.ParseError(errors, file.Lines.FileName), ct) + notifications.Trigger(NotificationEvent.ParseError(errors, file.Source.FileName), ct) }, ct ) @@ -1265,7 +1263,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar openFilesToChangesAndProjectOptions |> AMapAsync.mapAsyncAVal (fun _ (info, projectOptions) ctok -> asyncAVal { - let file = info.Lines.FileName + let file = info.Source.FileName let! checker = checker return! @@ -1287,13 +1285,13 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar openFilesToChangesAndProjectOptions |> AMapAsync.mapAsyncAVal (fun _ (info, projectOptions) _ -> asyncAVal { - let file = info.Lines.FileName + let file = info.Source.FileName let! checker = checker return option { let! opts = selectProject projectOptions - return! checker.TryGetRecentCheckResultsForFile(file, opts, info.Lines) + return! checker.TryGetRecentCheckResultsForFile(file, opts, info.Source) } }) @@ -1301,7 +1299,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar openFilesToChangesAndProjectOptions |> AMapAsync.mapAsyncAVal (fun _ (info, projectOptions) ctok -> asyncAVal { - let file = info.Lines.FileName + let file = info.Source.FileName let! checker = checker return! @@ -1326,7 +1324,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar let getRecentTypeCheckResults filePath = openFilesToRecentCheckedFilesResults |> AMapAsync.tryFindAndFlatten (filePath) - let tryGetLineStr pos (text: NamedText) = + let tryGetLineStr pos (text: IFSACSourceText) = text.GetLine(pos) |> Result.ofOption (fun () -> $"No line in {text.FileName} at position {pos}") @@ -1441,7 +1439,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar let! (text) = forceFindOpenFileOrRead file |> Async.map Option.ofResult try - let! line = text.Lines.GetLine(Position.mkPos i 0) + let! line = text.Source.GetLine(Position.mkPos i 0) return Lexer.tokenizeLine [||] line with _ -> return! None @@ -1451,7 +1449,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar asyncOption { try let! (text) = forceFindOpenFileOrRead file |> Async.map Option.ofResult - let! line = tryGetLineStr pos text.Lines |> Option.ofResult + let! line = tryGetLineStr pos text.Source |> Option.ofResult return! Lexer.getSymbol pos.Line pos.Column line SymbolLookupKind.Fuzzy [||] with _ -> return! None @@ -1463,7 +1461,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar if symbol.Kind = kind then let! (text) = forceFindOpenFileOrRead fileName |> Async.map Option.ofResult - let! line = tryGetLineStr pos text.Lines |> Option.ofResult + let! line = tryGetLineStr pos text.Source |> Option.ofResult let! tyRes = forceGetTypeCheckResults fileName |> Async.map (Option.ofResult) let symbolUse = tyRes.TryGetSymbolUse pos line return! Some(symbol, symbolUse) @@ -1541,7 +1539,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar else // untitled script files match! forceGetTypeCheckResultsStale file with - | Error _ -> return [||] + | Error _ -> return Seq.empty | Ok tyRes -> let! ct = Async.CancellationToken let usages = tyRes.GetCheckResults.GetUsesOfSymbolInFile(symbol, ct) @@ -1567,26 +1565,25 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar let codefixes = - let getFileLines = forceFindSourceText let tryGetParseResultsForFile filePath pos = asyncResult { let! (file) = forceFindOpenFileOrRead filePath - let! lineStr = file.Lines |> tryGetLineStr pos + let! lineStr = file.Source |> tryGetLineStr pos and! tyRes = forceGetTypeCheckResults filePath - return tyRes, lineStr, file.Lines + return tyRes, lineStr, file.Source } let getRangeText fileName (range: Ionide.LanguageServerProtocol.Types.Range) = asyncResult { - let! lines = getFileLines fileName - return! lines.GetText(protocolRangeToRange (UMX.untag fileName) range) + let! sourceText = forceFindSourceText fileName + return! sourceText.GetText(protocolRangeToRange (UMX.untag fileName) range) } let tryFindUnionDefinitionFromPos = tryFindUnionDefinitionFromPos codeGenServer - let getUnionPatternMatchCases tyRes pos lines line = - Commands.getUnionPatternMatchCases tryFindUnionDefinitionFromPos tyRes pos lines line + let getUnionPatternMatchCases tyRes pos sourceText line = + Commands.getUnionPatternMatchCases tryFindUnionDefinitionFromPos tyRes pos sourceText line let unionCaseStubReplacements (config) () = Map.ofList [ "$1", config.UnionCaseStubGenerationBody ] @@ -1603,11 +1600,11 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar let tryFindRecordDefinitionFromPos = RecordStubGenerator.tryFindRecordDefinitionFromPos codeGenServer - let getRecordStub tyRes pos lines line = - Commands.getRecordStub (tryFindRecordDefinitionFromPos) tyRes pos lines line + let getRecordStub tyRes pos sourceText line = + Commands.getRecordStub (tryFindRecordDefinitionFromPos) tyRes pos sourceText line - let getLineText (lines: NamedText) (range: Ionide.LanguageServerProtocol.Types.Range) = - lines.GetText(protocolRangeToRange (UMX.untag lines.FileName) range) + let getLineText (sourceText: IFSACSourceText) (range: Ionide.LanguageServerProtocol.Types.Range) = + sourceText.GetText(protocolRangeToRange (UMX.untag sourceText.FileName) range) |> Async.singleton let abstractClassStubReplacements config () = @@ -1621,19 +1618,19 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar let writeAbstractClassStub = AbstractClassStubGenerator.writeAbstractClassStub codeGenServer - let getAbstractClassStub tyRes objExprRange lines lineStr = + let getAbstractClassStub tyRes objExprRange sourceText lineStr = Commands.getAbstractClassStub tryFindAbstractClassExprInBufferAtPos writeAbstractClassStub tyRes objExprRange - lines + sourceText lineStr |> AsyncResult.foldResult id id config |> AVal.map (fun config -> - [| Run.ifEnabled (fun _ -> config.UnusedOpensAnalyzer) (RemoveUnusedOpens.fix getFileLines) + [| Run.ifEnabled (fun _ -> config.UnusedOpensAnalyzer) (RemoveUnusedOpens.fix forceFindSourceText) Run.ifEnabled (fun _ -> config.ResolveNamespaces) (ResolveNamespace.fix tryGetParseResultsForFile Commands.getNamespaceSuggestions) @@ -1644,7 +1641,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar Run.ifEnabled (fun _ -> config.UnionCaseStubGeneration) (GenerateUnionCases.fix - getFileLines + forceFindSourceText tryGetParseResultsForFile getUnionPatternMatchCases (unionCaseStubReplacements config)) @@ -1662,8 +1659,8 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar tryGetParseResultsForFile getAbstractClassStub (abstractClassStubReplacements config)) - AddMissingEqualsToTypeDefinition.fix getFileLines - ChangePrefixNegationToInfixSubtraction.fix getFileLines + AddMissingEqualsToTypeDefinition.fix forceFindSourceText + ChangePrefixNegationToInfixSubtraction.fix forceFindSourceText ConvertDoubleEqualsToSingleEquals.fix getRangeText ChangeEqualsInFieldTypeToColon.fix WrapExpressionInParentheses.fix getRangeText @@ -1674,9 +1671,9 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar ConvertInvalidRecordToAnonRecord.fix tryGetParseResultsForFile RemoveUnnecessaryReturnOrYield.fix tryGetParseResultsForFile getLineText ConvertCSharpLambdaToFSharpLambda.fix tryGetParseResultsForFile getLineText - AddMissingFunKeyword.fix getFileLines getLineText + AddMissingFunKeyword.fix forceFindSourceText getLineText MakeOuterBindingRecursive.fix tryGetParseResultsForFile getLineText - AddMissingRecKeyword.fix getFileLines getLineText + AddMissingRecKeyword.fix forceFindSourceText getLineText ConvertBangEqualsToInequality.fix getRangeText ChangeDerefBangToValue.fix tryGetParseResultsForFile getLineText RemoveUnusedBinding.fix tryGetParseResultsForFile @@ -1877,8 +1874,8 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar ( fileName: string, action: unit -> Async>, - handlerFormattedDoc: (NamedText * string) -> TextEdit[], - handleFormattedRange: (NamedText * string * FormatSelectionRange) -> TextEdit[] + handlerFormattedDoc: (IFSACSourceText * string) -> TextEdit[], + handleFormattedRange: (IFSACSourceText * string * FormatSelectionRange) -> TextEdit[] ) : Async>> = asyncResult { let tags = [ SemanticConventions.fsac_sourceCodePath, box fileName ] @@ -1892,12 +1889,12 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar let rootPath = rootPath |> AVal.force match res with - | (FormatDocumentResponse.Formatted(lines, formatted)) -> - let result = handlerFormattedDoc (lines, formatted) + | (FormatDocumentResponse.Formatted(sourceText, formatted)) -> + let result = handlerFormattedDoc (sourceText, formatted) return (Some(result)) - | (FormatDocumentResponse.FormattedRange(lines, formatted, range)) -> - let result = handleFormattedRange (lines, formatted, range) + | (FormatDocumentResponse.FormattedRange(sourceText, formatted, range)) -> + let result = handleFormattedRange (sourceText, formatted, range) return (Some(result)) | FormatDocumentResponse.Ignored -> @@ -2167,7 +2164,9 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar return () else // We want to try to use the file system's datetime if available - let file = VolatileFile.Create(filePath, doc.Text, (Some doc.Version)) + let file = + VolatileFile.Create(sourceTextFactory.Create(filePath, doc.Text), doc.Version) + updateOpenFiles file let! _ = forceGetTypeCheckResults filePath return () @@ -2260,12 +2259,20 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar let file = option { let! oldFile = forceFindOpenFile filePath - let oldFile = p.Text |> Option.map (oldFile.SetText) |> Option.defaultValue oldFile + + let oldFile = + p.Text + |> Option.map (fun t -> sourceTextFactory.Create(oldFile.FileName, t)) + |> Option.map (oldFile.SetSource) + |> Option.defaultValue oldFile + return oldFile.UpdateTouched() } |> Option.defaultWith (fun () -> // Very unlikely to get here - VolatileFile.Create(filePath, p.Text.Value, None, DateTime.UtcNow)) + VolatileFile.Create(sourceTextFactory.Create(filePath, p.Text.Value)) + + ) transact (fun () -> updateOpenFiles file @@ -2306,9 +2313,9 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar let (filePath, pos) = getFilePathAndPosition p - let! (namedText2) = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr + let! volatileFile = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr2 = namedText2.Lines |> tryGetLineStr pos |> Result.ofStringErr + let! lineStr2 = volatileFile.Source |> tryGetLineStr pos |> Result.ofStringErr if lineStr2.StartsWith "#" then let completionList = @@ -2333,8 +2340,8 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar let getCompletions = asyncResult { - let! (namedText) = forceFindOpenFileOrRead filePath - let! lineStr = namedText.Lines |> tryGetLineStr pos + let! volatileFile = forceFindOpenFileOrRead filePath + let! lineStr = volatileFile.Source |> tryGetLineStr pos and! typeCheckResults = forceGetTypeCheckResultsStale filePath let getAllSymbols () = @@ -2348,7 +2355,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar do! match p.Context with | Some({ triggerKind = CompletionTriggerKind.TriggerCharacter } as context) -> - namedText.Lines.TryGetChar pos = context.triggerCharacter + volatileFile.Source.TryGetChar pos = context.triggerCharacter | _ -> true |> Result.requireTrue $"TextDocumentCompletion was sent before TextDocumentDidChange" @@ -2358,7 +2365,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar typeCheckResults.TryGetCompletions pos lineStr None getAllSymbols |> AsyncResult.ofOption (fun () -> "No TryGetCompletions results")) - return Some(decls, residue, shouldKeywords, typeCheckResults, getAllSymbols, namedText) + return Some(decls, residue, shouldKeywords, typeCheckResults, getAllSymbols, volatileFile) } match! @@ -2366,7 +2373,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar |> AsyncResult.ofStringErr with | None -> return! success (None) - | Some(decls, _, shouldKeywords, typeCheckResults, _, namedText) -> + | Some(decls, _, shouldKeywords, typeCheckResults, _, volatileFile) -> return! Debug.measure "TextDocumentCompletion.TryGetCompletions success" @@ -2374,7 +2381,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar transact (fun () -> HashMap.OfList( [ for d in decls do - d.NameInList, (d, pos, filePath, namedText.Lines.GetLine, typeCheckResults.GetAST) ] + d.NameInList, (d, pos, filePath, volatileFile.Source.GetLine, typeCheckResults.GetAST) ] ) |> autoCompleteItems.UpdateTo) |> ignore @@ -2540,7 +2547,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar let (filePath, pos) = getFilePathAndPosition p - let! (namedText) = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr + let! volatileFile = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr and! tyRes = forceGetTypeCheckResults filePath |> AsyncResult.ofStringErr @@ -2548,7 +2555,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar let charAtCaret = p.Context |> Option.bind (fun c -> c.TriggerCharacter) match! - SignatureHelp.getSignatureHelpFor (tyRes, pos, namedText.Lines, charAtCaret, None) + SignatureHelp.getSignatureHelpFor (tyRes, pos, volatileFile.Source, charAtCaret, None) |> AsyncResult.ofStringErr with | None -> @@ -2604,8 +2611,8 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar ) let (filePath, pos) = getFilePathAndPosition p - let! (namedText) = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = namedText.Lines |> tryGetLineStr pos |> Result.ofStringErr + let! volatileFile = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr + let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.ofStringErr and! tyRes = forceGetTypeCheckResultsStale filePath |> AsyncResult.ofStringErr match tyRes.TryGetToolTipEnhanced pos lineStr with @@ -2697,12 +2704,12 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar ) let (filePath, pos) = getFilePathAndPosition p - let! namedText = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = namedText.Lines |> tryGetLineStr pos |> Result.ofStringErr + let! volatileFile = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr + let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.ofStringErr let! tyRes = forceGetTypeCheckResults filePath |> AsyncResult.ofStringErr let! (_, _, range) = - Commands.renameSymbolRange getDeclarationLocation false pos lineStr namedText.Lines tyRes + Commands.renameSymbolRange getDeclarationLocation false pos lineStr volatileFile.Source tyRes |> AsyncResult.mapError (fun msg -> JsonRpc.Error.Create(JsonRpc.ErrorCodes.invalidParams, msg)) return range |> fcsRangeToLsp |> PrepareRenameResult.Range |> Some @@ -2720,22 +2727,22 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar ) let (filePath, pos) = getFilePathAndPosition p - let! (namedText) = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = namedText.Lines |> tryGetLineStr pos |> Result.ofStringErr + let! volatileFile = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr + let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.ofStringErr and! tyRes = forceGetTypeCheckResults filePath |> AsyncResult.ofStringErr // validate name and surround with backticks if necessary let! newName = - Commands.adjustRenameSymbolNewName pos lineStr namedText.Lines tyRes p.NewName + Commands.adjustRenameSymbolNewName pos lineStr volatileFile.Source tyRes p.NewName |> AsyncResult.mapError (fun msg -> JsonRpc.Error.Create(JsonRpc.ErrorCodes.invalidParams, msg)) // safety check: rename valid? let! _ = - Commands.renameSymbolRange getDeclarationLocation false pos lineStr namedText.Lines tyRes + Commands.renameSymbolRange getDeclarationLocation false pos lineStr volatileFile.Source tyRes |> AsyncResult.mapError (fun msg -> JsonRpc.Error.Create(JsonRpc.ErrorCodes.invalidParams, msg)) let! (_, ranges) = - symbolUseWorkspace true true true pos lineStr namedText.Lines tyRes + symbolUseWorkspace true true true pos lineStr volatileFile.Source tyRes |> AsyncResult.mapError (fun msg -> JsonRpc.Error.Create(JsonRpc.ErrorCodes.invalidParams, msg)) let! documentChanges = @@ -2791,9 +2798,9 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar ) let (filePath, pos) = getFilePathAndPosition p - let! (namedText) = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr + let! volatileFile = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = namedText.Lines |> tryGetLineStr pos |> Result.ofStringErr + let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.ofStringErr and! tyRes = forceGetTypeCheckResults filePath |> AsyncResult.ofStringErr let! decl = tyRes.TryFindDeclaration pos lineStr |> AsyncResult.ofStringErr return decl |> findDeclToLspLocation |> GotoResult.Single |> Some @@ -2822,8 +2829,8 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar let (filePath, pos) = getFilePathAndPosition p - let! (namedText) = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = namedText.Lines |> tryGetLineStr pos |> Result.ofStringErr + let! volatileFile = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr + let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.ofStringErr and! tyRes = forceGetTypeCheckResults filePath |> AsyncResult.ofStringErr let! decl = tyRes.TryFindTypeDeclaration pos lineStr |> AsyncResult.ofStringErr return decl |> findDeclToLspLocation |> GotoResult.Single |> Some @@ -2851,12 +2858,12 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar ) let (filePath, pos) = getFilePathAndPosition p - let! (namedText) = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = tryGetLineStr pos namedText.Lines |> Result.ofStringErr + let! volatileFile = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr + let! lineStr = tryGetLineStr pos volatileFile.Source |> Result.ofStringErr and! tyRes = forceGetTypeCheckResults filePath |> AsyncResult.ofStringErr let! (_, usages) = - symbolUseWorkspace true true false pos lineStr namedText.Lines tyRes + symbolUseWorkspace true true false pos lineStr volatileFile.Source tyRes |> AsyncResult.mapError (JsonRpc.Error.InternalErrorMessage) let references = @@ -2887,8 +2894,8 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar ) let (filePath, pos) = getFilePathAndPosition p - let! (namedText) = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = tryGetLineStr pos namedText.Lines |> Result.ofStringErr + let! volatileFile = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr + let! lineStr = tryGetLineStr pos volatileFile.Source |> Result.ofStringErr and! tyRes = forceGetTypeCheckResults filePath |> AsyncResult.ofStringErr match @@ -2929,8 +2936,8 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar ) let (filePath, pos) = getFilePathAndPosition p - let! (namedText) = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = tryGetLineStr pos namedText.Lines |> Result.ofStringErr + let! volatileFile = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr + let! lineStr = tryGetLineStr pos volatileFile.Source |> Result.ofStringErr and! tyRes = forceGetTypeCheckResults filePath |> AsyncResult.ofStringErr logger.info ( @@ -3082,10 +3089,10 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar let formatDocumentAsync x = fantomasService.FormatDocumentAsync x Commands.formatDocument tryGetFileCheckerOptionsWithLines formatDocumentAsync fileName - let handlerFormattedDoc (lines: NamedText, formatted: string) = + let handlerFormattedDoc (sourceText: IFSACSourceText, formatted: string) = let range = let zero = { Line = 0; Character = 0 } - let lastPos = lines.LastFilePosition + let lastPos = sourceText.LastFilePosition { Start = zero End = fcsPosToLsp lastPos } @@ -3133,7 +3140,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar Commands.formatSelection tryGetFileCheckerOptionsWithLines formatSelectionAsync fileName range - let handlerFormattedRangeDoc (lines: NamedText, formatted: string, range: FormatSelectionRange) = + let handlerFormattedRangeDoc (sourceText: IFSACSourceText, formatted: string, range: FormatSelectionRange) = let range = { Start = { Line = range.StartLine - 1 @@ -3291,11 +3298,11 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar >> Log.addContextDestructured "file" filePath ) - let! (namedText: NamedText) = forceFindSourceText filePath |> AsyncResult.ofStringErr - let! lineStr = namedText |> tryGetLineStr pos |> Result.ofStringErr + let! (sourceText: IFSACSourceText) = forceFindSourceText filePath |> AsyncResult.ofStringErr + let! lineStr = sourceText |> tryGetLineStr pos |> Result.ofStringErr let typ = data.[1] - let! r = Async.Catch(f arg pos tyRes namedText lineStr typ filePath) + let! r = Async.Catch(f arg pos tyRes sourceText lineStr typ filePath) match r with | Choice1Of2(r: LspResult) -> @@ -3339,7 +3346,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar JToken.FromObject(usageLocations |> Array.map fcsRangeToLspLocation) |] handler - (fun p pos tyRes lines lineStr typ file -> + (fun p pos tyRes sourceText lineStr typ file -> async { if typ = "signature" then match @@ -3367,7 +3374,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar return { p with Command = Some cmd } |> Some |> success elif typ = "reference" then let! uses = - symbolUseWorkspace false true false pos lineStr lines tyRes + symbolUseWorkspace false true false pos lineStr sourceText tyRes |> AsyncResult.mapError (JsonRpc.Error.InternalErrorMessage) match uses with @@ -3483,9 +3490,9 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar let getParseResultsForFile file = asyncResult { - let! namedText = forceFindSourceText file + let! sourceText = forceFindSourceText file and! parseResults = forceGetParseResults file - return namedText, parseResults + return sourceText, parseResults } let! scopes = Commands.scopesForFile getParseResultsForFile file |> AsyncResult.ofStringErr @@ -3614,7 +3621,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar ) let filePath = p.TextDocument.GetFilePath() |> Utils.normalizePath - let! (namedText) = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr + let! volatileFile = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr and! tyRes = forceGetTypeCheckResults filePath |> AsyncResult.ofStringErr @@ -3623,7 +3630,7 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar let! hints = Commands.InlayHints( - namedText.Lines, + volatileFile.Source, tyRes, fcsRange, showTypeHints = config.InlayHints.typeAnnotations, @@ -3719,14 +3726,14 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar ) let filePath = p.TextDocument.GetFilePath() |> Utils.normalizePath - let! (namedText) = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr + let! volatileFile = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr let! tyRes = forceGetTypeCheckResults filePath |> AsyncResult.ofStringErr let fcsRange = protocolRangeToRange (UMX.untag filePath) p.Range - let! pipelineHints = Commands.InlineValues(namedText.Lines, tyRes) + let! pipelineHints = Commands.InlineValues(volatileFile.Source, tyRes) let hints = pipelineHints @@ -3923,9 +3930,9 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar ) let (filePath, pos) = getFilePathAndPosition p - let! (namedText) = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr + let! volatileFile = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = namedText.Lines |> tryGetLineStr pos |> Result.ofStringErr + let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.ofStringErr and! tyRes = forceGetTypeCheckResults filePath |> AsyncResult.ofStringErr let! tip = Commands.typesig tyRes pos lineStr |> Result.ofCoreResponse @@ -3959,8 +3966,8 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar FSharp.Compiler.Text.Position.mkPos (p.Position.Line) (p.Position.Character + 2) let filePath = p.TextDocument.GetFilePath() |> Utils.normalizePath - let! (namedText) = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = namedText.Lines |> tryGetLineStr pos |> Result.ofStringErr + let! volatileFile = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr + let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.ofStringErr and! tyRes = forceGetTypeCheckResults filePath |> AsyncResult.ofStringErr let! (typ, parms, generics) = tyRes.TryGetSignatureData pos lineStr |> Result.ofStringErr @@ -3994,9 +4001,9 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar ) let (filePath, pos) = getFilePathAndPosition p - let! (namedText) = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr + let! volatileFile = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = namedText.Lines |> tryGetLineStr pos |> Result.ofStringErr + let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.ofStringErr and! tyRes = forceGetTypeCheckResults filePath |> AsyncResult.ofStringErr match! @@ -4342,8 +4349,8 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar ) let (filePath, pos) = getFilePathAndPosition p - let! (namedText) = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = namedText.Lines |> tryGetLineStr pos |> Result.ofStringErr + let! volatileFile = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr + let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.ofStringErr and! tyRes = forceGetTypeCheckResults filePath |> AsyncResult.ofStringErr match! Commands.Help tyRes pos lineStr |> Result.ofCoreResponse with @@ -4373,8 +4380,8 @@ type AdaptiveFSharpLspServer(workspaceLoader: IWorkspaceLoader, lspClient: FShar ) let (filePath, pos) = getFilePathAndPosition p - let! (namedText) = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr - let! lineStr = namedText.Lines |> tryGetLineStr pos |> Result.ofStringErr + let! volatileFile = forceFindOpenFileOrRead filePath |> AsyncResult.ofStringErr + let! lineStr = volatileFile.Source |> tryGetLineStr pos |> Result.ofStringErr and! tyRes = forceGetTypeCheckResults filePath |> AsyncResult.ofStringErr lastFSharpDocumentationTypeCheck <- Some tyRes @@ -4827,7 +4834,7 @@ module AdaptiveFSharpLspServer = | HandleableException -> false | _ -> true } - let startCore toolsPath workspaceLoaderFactory = + let startCore toolsPath workspaceLoaderFactory sourceTextFactory = use input = Console.OpenStandardInput() use output = Console.OpenStandardOutput() @@ -4863,6 +4870,6 @@ module AdaptiveFSharpLspServer = let adaptiveServer lspClient = let loader = workspaceLoaderFactory toolsPath - new AdaptiveFSharpLspServer(loader, lspClient) :> IFSharpLspServer + new AdaptiveFSharpLspServer(loader, lspClient, sourceTextFactory) :> IFSharpLspServer Ionide.LanguageServerProtocol.Server.start requestsHandlings input output FSharpLspClient adaptiveServer createRpc diff --git a/src/FsAutoComplete/LspServers/FsAutoComplete.Lsp.fs b/src/FsAutoComplete/LspServers/FsAutoComplete.Lsp.fs index 1d2e6e3ea..7ffad8fdd 100644 --- a/src/FsAutoComplete/LspServers/FsAutoComplete.Lsp.fs +++ b/src/FsAutoComplete/LspServers/FsAutoComplete.Lsp.fs @@ -35,7 +35,7 @@ open Ionide.LanguageServerProtocol.Types.LspResult open StreamJsonRpc -type FSharpLspServer(state: State, lspClient: FSharpLspClient) = +type FSharpLspServer(state: State, lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFactory) = let logger = LogProvider.getLoggerByName "LSP" @@ -44,7 +44,7 @@ type FSharpLspServer(state: State, lspClient: FSharpLspClient) = let mutable rootPath: string option = None let mutable commands = - new Commands(FSharpCompilerServiceChecker(false, 200L), state, false, rootPath) + new Commands(FSharpCompilerServiceChecker(false, 200L), state, false, rootPath, sourceTextFactory) let mutable commandDisposables = ResizeArray() let mutable clientCapabilities: ClientCapabilities option = None @@ -86,7 +86,7 @@ type FSharpLspServer(state: State, lspClient: FSharpLspClient) = /// `DateTime` instead of `Stopwatch`: stopwatch doesn't work with multiple simultaneous consumers let mutable lastCheckFile = DateTime.UtcNow - let checkFile (filePath: string, version: int, content: NamedText, isFirstOpen: bool) = + let checkFile (filePath: string, version: int, content: IFSACSourceText, isFirstOpen: bool) = asyncResult { let start = @@ -482,7 +482,8 @@ type FSharpLspServer(state: State, lspClient: FSharpLspClient) = FSharpCompilerServiceChecker(hasAnalyzersNow, config.Fsac.CachedTypeCheckCount), state, hasAnalyzersNow, - rootPath + rootPath, + sourceTextFactory ) commands <- newCommands @@ -618,7 +619,7 @@ type FSharpLspServer(state: State, lspClient: FSharpLspClient) = ///Helper function for handling Position requests using **recent** type check results member x.positionHandler<'a, 'b when 'b :> ITextDocumentPositionParams> - (f: 'b -> FcsPos -> ParseAndCheckResults -> string -> NamedText -> AsyncLspResult<'a>) + (f: 'b -> FcsPos -> ParseAndCheckResults -> string -> IFSACSourceText -> AsyncLspResult<'a>) (arg: 'b) : AsyncLspResult<'a> = async { @@ -671,7 +672,7 @@ type FSharpLspServer(state: State, lspClient: FSharpLspClient) = ///Helper function for handling file requests using **recent** type check results member x.fileHandler<'a> - (f: string -> ParseAndCheckResults -> NamedText -> AsyncLspResult<'a>) + (f: string -> ParseAndCheckResults -> IFSACSourceText -> AsyncLspResult<'a>) (arg: TextDocumentIdentifier) : AsyncLspResult<'a> = async { @@ -726,8 +727,8 @@ type FSharpLspServer(state: State, lspClient: FSharpLspClient) = ( fileName: string, action: unit -> Async>, - handlerFormattedDoc: (NamedText * string) -> TextEdit[], - handleFormattedRange: (NamedText * string * FormatSelectionRange) -> TextEdit[] + handlerFormattedDoc: (IFSACSourceText * string) -> TextEdit[], + handleFormattedRange: (IFSACSourceText * string * FormatSelectionRange) -> TextEdit[] ) = async { let! res = action () @@ -1108,7 +1109,7 @@ type FSharpLspServer(state: State, lspClient: FSharpLspClient) = let getFileLines = commands.TryGetFileCheckerOptionsWithLines >> Result.map snd >> Async.singleton - let getLineText (lines: NamedText) (range: Ionide.LanguageServerProtocol.Types.Range) = + let getLineText (lines: IFSACSourceText) (range: Ionide.LanguageServerProtocol.Types.Range) = lines.GetText(protocolRangeToRange (UMX.untag lines.FileName) range) |> Async.singleton @@ -1298,7 +1299,7 @@ type FSharpLspServer(state: State, lspClient: FSharpLspClient) = async { let doc = p.TextDocument let filePath = doc.GetFilePath() |> Utils.normalizePath - let content = NamedText(filePath, doc.Text) + let content = sourceTextFactory.Create(filePath, doc.Text) logger.info ( Log.setMessage "TextDocumentDidOpen Request: {parms}" @@ -1332,21 +1333,14 @@ type FSharpLspServer(state: State, lspClient: FSharpLspClient) = let initialText = state.TryGetFileSource(filePath) - |> Result.bimap id (fun _ -> NamedText(filePath, "")) + |> Result.bimap id (fun _ -> sourceTextFactory.Create(filePath, "")) let evolvedFileContent = (initialText, p.ContentChanges) ||> Array.fold (fun text change -> match change.Range with | None -> // replace entire content - NamedText(filePath, change.Text) - | Some rangeToReplace when - rangeToReplace.Start.Line = 0 - && rangeToReplace.Start.Character = 0 - && rangeToReplace.End.Line = 0 - && rangeToReplace.End.Character = 0 - -> - NamedText(filePath, change.Text) + sourceTextFactory.Create(filePath, change.Text) | Some rangeToReplace -> // replace just this slice let fcsRangeToReplace = protocolRangeToRange (UMX.untag filePath) rangeToReplace @@ -1861,7 +1855,7 @@ type FSharpLspServer(state: State, lspClient: FSharpLspClient) = commands.FormatDocument fileName - let handlerFormattedDoc (lines: NamedText, formatted: string) = + let handlerFormattedDoc (lines: IFSACSourceText, formatted: string) = let range = let zero = { Line = 0; Character = 0 } let lastPos = lines.LastFilePosition @@ -1893,7 +1887,7 @@ type FSharpLspServer(state: State, lspClient: FSharpLspClient) = commands.FormatSelection(fileName, range) - let handlerFormattedRangeDoc (lines: NamedText, formatted: string, range: FormatSelectionRange) = + let handlerFormattedRangeDoc (lines: IFSACSourceText, formatted: string, range: FormatSelectionRange) = let range = { Start = { Line = range.StartLine - 1 @@ -2977,7 +2971,7 @@ module FSharpLspServer = | HandleableException -> false | _ -> true } - let startCore toolsPath stateStorageDir workspaceLoaderFactory = + let startCore toolsPath stateStorageDir workspaceLoaderFactory sourceTextFactory = use input = Console.OpenStandardInput() use output = Console.OpenStandardOutput() @@ -3015,7 +3009,7 @@ module FSharpLspServer = let state = State.Initial toolsPath stateStorageDir workspaceLoaderFactory let originalFs = FSharp.Compiler.IO.FileSystemAutoOpens.FileSystem FSharp.Compiler.IO.FileSystemAutoOpens.FileSystem <- FsAutoComplete.FileSystem(originalFs, state.Files.TryFind) - new FSharpLspServer(state, lspClient) :> IFSharpLspServer + new FSharpLspServer(state, lspClient, sourceTextFactory) :> IFSharpLspServer Ionide.LanguageServerProtocol.Server.start requestsHandlings input output FSharpLspClient regularServer createRpc diff --git a/src/FsAutoComplete/Parser.fs b/src/FsAutoComplete/Parser.fs index 6e1d4d99a..2b8f98f17 100644 --- a/src/FsAutoComplete/Parser.fs +++ b/src/FsAutoComplete/Parser.fs @@ -16,6 +16,12 @@ open OpenTelemetry open OpenTelemetry.Resources open OpenTelemetry.Trace +type SourceTextFactoryOptions = + | NamedText = 0 + | RoslynSourceText = 1 + + + module Parser = open FsAutoComplete.Core open System.Diagnostics @@ -97,6 +103,14 @@ module Parser = "Enable LSP Server based on FSharp.Data.Adaptive. Should be more stable, but is experimental." ) + let sourceTextFactoryOption = + Option( + "--source-text-factory", + description = + "Set the source text factory to use. NamedText is the default, and uses an old F# compiler's implementation. RoslynSourceText uses Roslyn's implementation.", + getDefaultValue = fun () -> SourceTextFactoryOptions.NamedText + ) + let otelTracingOption = Option( "--otel-exporter-enabled", @@ -124,12 +138,13 @@ module Parser = rootCommand.AddOption logLevelOption rootCommand.AddOption stateLocationOption rootCommand.AddOption otelTracingOption + rootCommand.AddOption sourceTextFactoryOption // for back-compat - we removed some options and this broke some clients. rootCommand.TreatUnmatchedTokensAsErrors <- false rootCommand.SetHandler( - Func<_, _, _, Task>(fun projectGraphEnabled stateDirectory adaptiveLspEnabled -> + Func<_, _, _, _, Task>(fun projectGraphEnabled stateDirectory adaptiveLspEnabled sourceTextFactoryOption -> let workspaceLoaderFactory = fun toolsPath -> if projectGraphEnabled then @@ -137,6 +152,12 @@ module Parser = else Ionide.ProjInfo.WorkspaceLoader.Create(toolsPath, ProjectLoader.globalProperties) + let sourceTextFactory: ISourceTextFactory = + match sourceTextFactoryOption with + | SourceTextFactoryOptions.NamedText -> new NamedTextFactory() + | SourceTextFactoryOptions.RoslynSourceText -> new RoslynSourceTextFactory() + | _ -> new NamedTextFactory() + let dotnetPath = if Environment.ProcessPath.EndsWith("dotnet") @@ -152,9 +173,9 @@ module Parser = let lspFactory = if adaptiveLspEnabled then - fun () -> AdaptiveFSharpLspServer.startCore toolsPath workspaceLoaderFactory + fun () -> AdaptiveFSharpLspServer.startCore toolsPath workspaceLoaderFactory sourceTextFactory else - fun () -> FSharpLspServer.startCore toolsPath stateDirectory workspaceLoaderFactory + fun () -> FSharpLspServer.startCore toolsPath stateDirectory workspaceLoaderFactory sourceTextFactory use _compilerEventListener = new Debug.FSharpCompilerEventLogger.Listener() @@ -163,7 +184,8 @@ module Parser = Task.FromResult result), projectGraphOption, stateLocationOption, - adaptiveLspServerOption + adaptiveLspServerOption, + sourceTextFactoryOption ) rootCommand diff --git a/src/FsAutoComplete/paket.references b/src/FsAutoComplete/paket.references index 7e570da9c..e8e49943b 100644 --- a/src/FsAutoComplete/paket.references +++ b/src/FsAutoComplete/paket.references @@ -25,3 +25,4 @@ System.Configuration.ConfigurationManager FSharp.Data.Adaptive Microsoft.Extensions.Caching.Memory OpenTelemetry.Exporter.OpenTelemetryProtocol +Microsoft.CodeAnalysis diff --git a/test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs b/test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs index 4b9fc8e00..a242b5f61 100644 --- a/test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs @@ -16,7 +16,7 @@ open FsToolkit.ErrorHandling open FSharp.Compiler.CodeAnalysis open Helpers.Expecto.ShadowedTimeouts -let private scriptTests state = +let private scriptTests state = testList "script" [ let server = async { @@ -66,7 +66,7 @@ module private Cursor = let usageEnd = ">$" let defStart = "$D<" let defEnd = ">D$" - + let private extractRanges (sourceWithCursors: string) = let (source, cursors) = sourceWithCursors @@ -92,11 +92,11 @@ let private extractRanges (sourceWithCursors: string) = match cursors with | [] -> ranges | [(c,p)] -> failwith $"Lonely last cursor {c} at {p}" - | (c1,p1)::(c2,p2)::cursors when c1 = Cursor.usageStart && c2 = Cursor.usageEnd -> + | (c1,p1)::(c2,p2)::cursors when c1 = Cursor.usageStart && c2 = Cursor.usageEnd -> let range = mkRange p1 p2 let ranges = (range :: decls, usages) collectRanges cursors ranges - | (c1,p1)::(c2,p2) :: cursors when c1 = Cursor.defStart && c2 = Cursor.defEnd -> + | (c1,p1)::(c2,p2) :: cursors when c1 = Cursor.defStart && c2 = Cursor.defEnd -> let range = mkRange p1 p2 let ranges = (decls, range :: usages) collectRanges cursors ranges @@ -115,7 +115,7 @@ let private mkLocation doc range = } /// mark locations in text /// -> differences gets highlighted in source instead of Location array -/// +/// /// Locations are marked with `〈...〉` let private markRanges (source: string) (locs: Location[]) = let ranges = @@ -136,16 +136,16 @@ module Expect = /// * `true`: ranges of `actual` & `expected` must match exactly /// * `false`: ranges of `actual` must contain ranges of `expected` -> ranges in `actual` can cover a larger area /// * Reason: ranges only get adjusted iff source file was loaded (into `state` in `Commands`). - /// Because of background loading (& maybe changes in FSAC implementation) not always predictable what is and isn't loaded - /// -> Just check if correct range is covered - /// * Example: Find References for `map`: FCS returns range covering `List.map`. - /// That range gets reduced to just `map` iff source file is loaded (-> NamedText available). - /// But if file not loaded range stays `List.map`. - /// - /// -> - /// * Solution tests: multiple projects -> not everything loaded -> use `exact=false` + /// Because of background loading (& maybe changes in FSAC implementation) not always predictable what is and isn't loaded + /// -> Just check if correct range is covered + /// * Example: Find References for `map`: FCS returns range covering `List.map`. + /// That range gets reduced to just `map` iff source file is loaded (-> IFSACSourceText available). + /// But if file not loaded range stays `List.map`. + /// + /// -> + /// * Solution tests: multiple projects -> not everything loaded -> use `exact=false` /// -> tests for: every reference found? - /// * untitled & range tests: in untitled doc -> loaded because passed to FSAC -> use `exact=true` + /// * untitled & range tests: in untitled doc -> loaded because passed to FSAC -> use `exact=true` /// -> tests for: correct range found? let locationsEqual (getSource: DocumentUri -> string) (exact: bool) (actual: Location[]) (expected: Location[]) = let inspect () = @@ -198,31 +198,31 @@ module Expect = inspect () else inspect () - + let private solutionTests state = let marker = "//>" /// Format of Locations in file `path`: /// In line after range: /// * Mark start of line with `//>` - /// * underline range (in prev line) with a char-sequence (like `^^^^^`) + /// * underline range (in prev line) with a char-sequence (like `^^^^^`) /// * name after range marker (separated with single space from range) (name can contain spaces) /// -> results are grouped by named /// * no name: assigned empty string as name - /// + /// /// Example: /// ```fsharp /// let foo bar = /// //> ^^^ parameter /// 42 + bar /// //> ^^^ parameter usage - /// let alpha beta = + /// let alpha beta = /// //> ^^^^ parameter /// beta + 42 /// //> ^^^^ parameter usage /// ``` /// -> 4 locations, two `parameter` and two `parameter usage` - /// + /// /// Note: it's currently not possible to get two (or more) ranges for a single line! let readReferences path = let lines = File.ReadAllLines path @@ -234,7 +234,7 @@ let private solutionTests state = let splits = l.Split([|' '|], 2) let mark = splits[0] let ty = mark[0] - let range = + let range = let col = line.IndexOf mark let length = mark.Length let line = i - 1 // marker is line AFTER actual range @@ -249,7 +249,7 @@ let private solutionTests state = |> Path.LocalPathToUri Range = range } - let name = + let name = if splits.Length > 1 then splits[1] else @@ -263,12 +263,12 @@ let private solutionTests state = existing.Add loc |> ignore refs - let readAllReferences dir = + let readAllReferences dir = // `.fs` & `.fsx` let files = Directory.GetFiles(dir, "*.fs*", SearchOption.AllDirectories) files |> Seq.map readReferences - |> Seq.map (fun dict -> + |> Seq.map (fun dict -> dict |> Seq.map (fun kvp -> kvp.Key, kvp.Value) ) @@ -315,10 +315,10 @@ let private solutionTests state = do! assertScriptFilesLoaded let! (doc, _) = doc - let cursor = + let cursor = let cursor = r.Locations - |> Seq.filter (fun l -> l.Uri = doc.Uri) + |> Seq.filter (fun l -> l.Uri = doc.Uri) |> Seq.minBy (fun l -> l.Range.Start) cursor.Range.Start @@ -327,7 +327,7 @@ let private solutionTests state = Position = cursor Context = { IncludeDeclaration = true } } let! refs = doc.Server.Server.TextDocumentReferences request - let refs = + let refs = refs |> Flip.Expect.wantOk "Should not fail" |> Flip.Expect.wantSome "Should return references" @@ -337,7 +337,7 @@ let private solutionTests state = let getSource uri = let path = Path.FileUriToLocalPath uri File.ReadAllText path - + Expect.locationsEqual getSource false refs expected }) ]) @@ -349,7 +349,7 @@ let private untitledTests state = serverTestList "untitled" state defaultConfigDto None (fun server -> [ testCaseAsync "can find external `Delay` in all open untitled docs" (async { // Note: Cursor MUST be in first source - let sources = + let sources = [| """ open System @@ -358,13 +358,13 @@ let private untitledTests state = do! Task.$$ (TimeSpan.MaxValue) do! Task.$<``Delay``>$ (TimeSpan.MaxValue) do! System.Threading.Tasks.Task.$$ (TimeSpan.MaxValue) - do! + do! System .Threading .Tasks .Task .$$ (TimeSpan.MaxValue) - } + } """ """ open System @@ -395,11 +395,11 @@ let private untitledTests state = return doc }) |> Async.Sequential - - let (cursorDoc, cursor) = + + let (cursorDoc, cursor) = let cursors = Array.zip docs sources - |> Array.choose (fun (doc, (_, cursors)) -> + |> Array.choose (fun (doc, (_, cursors)) -> cursors.Cursor |> Option.map (fun cursor -> (doc, cursor)) ) @@ -449,7 +449,7 @@ let private rangeTests state = Position = cursors.Cursor.Value Context = { IncludeDeclaration = true } } let! refs = doc.Server.Server.TextDocumentReferences request - let refs = + let refs = refs |> Flip.Expect.wantOk "Should not fail" |> Flip.Expect.wantSome "Should return references" @@ -472,7 +472,7 @@ let private rangeTests state = """ module MyModule = let $DD$ = 42 - + open MyModule let _ = $$ + 42 let _ = $<``value``>$ + 42 @@ -488,13 +488,13 @@ let private rangeTests state = do! Task.$$ (TimeSpan.MaxValue) do! Task.$<``Delay``>$ (TimeSpan.MaxValue) do! System.Threading.Tasks.Task.$$ (TimeSpan.MaxValue) - do! + do! System .Threading .Tasks .Task .$$ (TimeSpan.MaxValue) - } + } """ testCaseAsync "can get range of variable with required backticks" <| checkRanges server @@ -578,7 +578,7 @@ let private rangeTests state = do! $$.Delay(TimeSpan.MaxValue) do! $$.``Delay`` (TimeSpan.MaxValue) do! System.Threading.Tasks.$$.Delay (TimeSpan.MaxValue) - do! + do! System .Threading .Tasks @@ -596,13 +596,13 @@ let tests state = testList "Find All References tests" [ ] -let tryFixupRangeTests = testList (nameof Tokenizer.tryFixupRange) [ +let tryFixupRangeTests (sourceTextFactoryName, sourceTextFactory : ISourceTextFactory) = testList ($"{nameof Tokenizer.tryFixupRange}.{sourceTextFactoryName}") [ let checker = lazy (FSharpChecker.Create()) - let getSymbolUses source cursor = async { + let getSymbolUses (source : string) cursor = async { let checker = checker.Value let file = "code.fsx" let path: string = UMX.tag file - let source = NamedText(path, source) + let source = sourceTextFactory.Create(path, source) let! (projOptions, _) = checker.GetProjectOptionsFromScript(file, source, assumeDotNetFramework=false) let! (parseResults, checkResults) = checker.ParseAndCheckFileInProject(file, 0, source, projOptions) @@ -655,7 +655,7 @@ let tryFixupRangeTests = testList (nameof Tokenizer.tryFixupRange) [ collectRanges cursors (range::ranges) | _ -> failtest $"Expected matching range pair '$<', '>$', but got: %A{cursors}" - let ranges = + let ranges = collectRanges cursors [] (source, cursor, ranges) @@ -664,7 +664,7 @@ let tryFixupRangeTests = testList (nameof Tokenizer.tryFixupRange) [ let (source, cursor, expected) = extractCursorAndRanges sourceWithCursorAndRanges let! (source, symbol, usages) = getSymbolUses source cursor - + let symbolNameCore = symbol.DisplayNameCore let actual = usages @@ -705,7 +705,7 @@ let tryFixupRangeTests = testList (nameof Tokenizer.tryFixupRange) [ Expect.equal (markRanges actual) (markRanges expected) - "Should be correct ranges" + "Should be correct ranges" } testCaseAsync "Active Pattern - simple" <| @@ -835,7 +835,7 @@ let tryFixupRangeTests = testList (nameof Tokenizer.tryFixupRange) [ let _ = ( ``$<|Hello World|_|>$`` ) 42 // linebreaks - let _r = + let _r = ( $<| ``Hello World`` @@ -843,9 +843,9 @@ let tryFixupRangeTests = testList (nameof Tokenizer.tryFixupRange) [ _ |>$ ) 42 - let _ = - ( - ``$<|Hello World|_|>$`` + let _ = + ( + ``$<|Hello World|_|>$`` ) 42 let _ = MyModule.($<|``Hello World``|_|>$) 42 @@ -858,7 +858,7 @@ let tryFixupRangeTests = testList (nameof Tokenizer.tryFixupRange) [ // let _ = MyModule.( ``|Hello World|_|`` ) 42 // linebreaks - let _r = + let _r = MyModule.( $<| ``Hello World`` @@ -867,9 +867,9 @@ let tryFixupRangeTests = testList (nameof Tokenizer.tryFixupRange) [ |>$ ) 42 // invalid - // let _ = - // MyModule.( - // ``|Hello World|_|`` + // let _ = + // MyModule.( + // ``|Hello World|_|`` // ) 42 """ testCaseAsync "Active Pattern - required backticks - with backticks" <| @@ -886,7 +886,7 @@ let tryFixupRangeTests = testList (nameof Tokenizer.tryFixupRange) [ let _ = ( $<``|Hello World|_|``>$ ) 42 // linebreaks - let _r = + let _r = ( $<| ``Hello World`` @@ -894,9 +894,9 @@ let tryFixupRangeTests = testList (nameof Tokenizer.tryFixupRange) [ _ |>$ ) 42 - let _ = - ( - $<``|Hello World|_|``>$ + let _ = + ( + $<``|Hello World|_|``>$ ) 42 let _ = MyModule.($<|``Hello World``|_|>$) 42 @@ -909,7 +909,7 @@ let tryFixupRangeTests = testList (nameof Tokenizer.tryFixupRange) [ // let _ = MyModule.( ``|Hello World|_|`` ) 42 // linebreaks - let _r = + let _r = MyModule.( $<| ``Hello World`` @@ -918,9 +918,9 @@ let tryFixupRangeTests = testList (nameof Tokenizer.tryFixupRange) [ |>$ ) 42 // invalid - // let _ = - // MyModule.( - // ``|Hello World|_|`` + // let _ = + // MyModule.( + // ``|Hello World|_|`` // ) 42 """ @@ -928,7 +928,7 @@ let tryFixupRangeTests = testList (nameof Tokenizer.tryFixupRange) [ check false """ module MyModule = - let (|$$|Odd|) v = + let (|$$|Odd|) v = if v % 2 = 0 then $$ else Odd do @@ -955,7 +955,7 @@ let tryFixupRangeTests = testList (nameof Tokenizer.tryFixupRange) [ check true """ module MyModule = - let (|$$|Odd|) v = + let (|$$|Odd|) v = if v % 2 = 0 then $$ else Odd do @@ -981,12 +981,12 @@ let tryFixupRangeTests = testList (nameof Tokenizer.tryFixupRange) [ testCaseAsync "Active Pattern Case - simple - at decl" <| // Somehow `FSharpSymbolUse.Symbol.DisplayNameCore` is empty -- but references correct Even symbol - // + // // Why? Cannot reproduce with just FCS -> happens just in FSAC check false """ module MyModule = - let (|$$|Odd|) v = + let (|$$|Odd|) v = if v % 2 = 0 then $$ else Odd do @@ -1013,7 +1013,7 @@ let tryFixupRangeTests = testList (nameof Tokenizer.tryFixupRange) [ check true """ module MyModule = - let (|$$|Odd|) v = + let (|$$|Odd|) v = if v % 2 = 0 then $$ else Odd do @@ -1036,13 +1036,13 @@ let tryFixupRangeTests = testList (nameof Tokenizer.tryFixupRange) [ | MyModule.$<``Even``>$ -> () | MyModule.``Odd`` -> () """ - + testCaseAsync "operator -.-" <| check false """ module MyModule = let ($<-.->$) a b = a - b - + let _ = 1 $<-$0.->$ 2 let _ = ($<-.->$) 1 2 // invalid: @@ -1050,29 +1050,29 @@ let tryFixupRangeTests = testList (nameof Tokenizer.tryFixupRange) [ let _ = ( $<-.->$ ) 1 2 - + let _ = MyModule.($<-.->$) 1 2 // linebreaks - let _ = + let _ = MyModule .($<-.->$) 1 2 - let _ = + let _ = MyModule. ($<-.->$) 1 2 - let _ = + let _ = MyModule .( $<-.->$ ) 1 2 - let _ = + let _ = MyModule .( $<-.->$ ) 1 2 - let _ = + let _ = MyModule. ( $<-.->$ @@ -1084,7 +1084,7 @@ let tryFixupRangeTests = testList (nameof Tokenizer.tryFixupRange) [ """ module MyModule = let ($<-.->$) a b = a - b - + let _ = 1 $<-$0.->$ 2 let _ = ($<-.->$) 1 2 // invalid: @@ -1092,29 +1092,29 @@ let tryFixupRangeTests = testList (nameof Tokenizer.tryFixupRange) [ let _ = ( $<-.->$ ) 1 2 - + let _ = MyModule.($<-.->$) 1 2 // linebreaks - let _ = + let _ = MyModule .($<-.->$) 1 2 - let _ = + let _ = MyModule. ($<-.->$) 1 2 - let _ = + let _ = MyModule .( $<-.->$ ) 1 2 - let _ = + let _ = MyModule .( $<-.->$ ) 1 2 - let _ = + let _ = MyModule. ( $<-.->$ diff --git a/test/FsAutoComplete.Tests.Lsp/Helpers.fs b/test/FsAutoComplete.Tests.Lsp/Helpers.fs index 4cbd49b9e..0fa3240fd 100644 --- a/test/FsAutoComplete.Tests.Lsp/Helpers.fs +++ b/test/FsAutoComplete.Tests.Lsp/Helpers.fs @@ -193,7 +193,7 @@ let record (cacher: Cacher<_>) = cacher.OnNext(name, payload) AsyncLspResult.success Unchecked.defaultof<_> -let createServer (state: unit -> State) = +let createServer (state: unit -> State) sourceTextFactory = let serverInteractions = new Cacher<_>() let recordNotifications = record serverInteractions @@ -206,10 +206,10 @@ let createServer (state: unit -> State) = let originalFs = FSharp.Compiler.IO.FileSystemAutoOpens.FileSystem let fs = FsAutoComplete.FileSystem(originalFs, innerState.Files.TryFind) FSharp.Compiler.IO.FileSystemAutoOpens.FileSystem <- fs - let server = new FSharpLspServer(innerState, client) + let server = new FSharpLspServer(innerState, client, sourceTextFactory) server :> IFSharpLspServer, serverInteractions :> ClientEvents -let createAdaptiveServer (workspaceLoader) = +let createAdaptiveServer workspaceLoader sourceTextFactory = let serverInteractions = new Cacher<_>() let recordNotifications = record serverInteractions @@ -219,7 +219,7 @@ let createAdaptiveServer (workspaceLoader) = let loader = workspaceLoader () let client = FSharpLspClient(recordNotifications, recordRequests) - let server = new AdaptiveFSharpLspServer(loader, client) + let server = new AdaptiveFSharpLspServer(loader, client, sourceTextFactory) server :> IFSharpLspServer, serverInteractions :> ClientEvents let defaultConfigDto: FSharpConfigDto = diff --git a/test/FsAutoComplete.Tests.Lsp/InlayHintTests.fs b/test/FsAutoComplete.Tests.Lsp/InlayHintTests.fs index 2a9ded897..e0c93c359 100644 --- a/test/FsAutoComplete.Tests.Lsp/InlayHintTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/InlayHintTests.fs @@ -1600,7 +1600,7 @@ open FSharp.UMX open FsAutoComplete.LspHelpers open Ionide.LanguageServerProtocol.Types -let explicitTypeInfoTests = +let explicitTypeInfoTests (sourceTextFactoryName, sourceTextFactory : ISourceTextFactory) = let file = "test.fsx" let checker = lazy (FSharpChecker.Create()) @@ -1627,7 +1627,7 @@ let explicitTypeInfoTests = let getExplicitTypeInfo (pos: Position) (text: string) = async { - let text = NamedText(UMX.tag file, text) + let text = sourceTextFactory.Create(UMX.tag file, text) let! ast = getAst text let pos = protocolPosToPos pos @@ -1772,7 +1772,7 @@ let explicitTypeInfoTests = testSequenced <| testList - "detect type and parens" + $"detect type and parens.{sourceTextFactoryName}" [ testList "Expr" [ testList diff --git a/test/FsAutoComplete.Tests.Lsp/Program.fs b/test/FsAutoComplete.Tests.Lsp/Program.fs index 1f25dcf6c..bc7dd54f4 100644 --- a/test/FsAutoComplete.Tests.Lsp/Program.fs +++ b/test/FsAutoComplete.Tests.Lsp/Program.fs @@ -15,6 +15,7 @@ open Ionide.ProjInfo open System.Threading open Serilog.Filters open System.IO +open FsAutoComplete Expect.defaultDiffPrinter <- Diff.colourisedDiff @@ -46,8 +47,8 @@ let fsharpLspServerFactory toolsPath workspaceLoaderFactory = Helpers.createServer createServer -let adaptiveLspServerFactory toolsPath workspaceLoaderFactory = - Helpers.createAdaptiveServer (fun () -> workspaceLoaderFactory toolsPath) +let adaptiveLspServerFactory toolsPath workspaceLoaderFactory sourceTextFactory = + Helpers.createAdaptiveServer (fun () -> workspaceLoaderFactory toolsPath) sourceTextFactory let lspServers = [ @@ -55,6 +56,11 @@ let lspServers = "AdaptiveLspServer", adaptiveLspServerFactory ] +let sourceTextFactories: (string * ISourceTextFactory) list = [ + "NamedText", NamedTextFactory() + "RosylinSourceText", RoslynSourceTextFactory() +] + let mutable toolsPath = Ionide.ProjInfo.Init.init (System.IO.DirectoryInfo Environment.CurrentDirectory) None @@ -63,63 +69,65 @@ let lspTests = "lsp" [ for (loaderName, workspaceLoaderFactory) in loaders do for (lspName, lspFactory) in lspServers do - testList - $"{loaderName}.{lspName}" - [ - Templates.tests () - let createServer () = - lspFactory toolsPath workspaceLoaderFactory - - initTests createServer - closeTests createServer - - Utils.Tests.Server.tests createServer - Utils.Tests.CursorbasedTests.tests createServer - - CodeLens.tests createServer - documentSymbolTest createServer - Completion.autocompleteTest createServer - Completion.autoOpenTests createServer - foldingTests createServer - tooltipTests createServer - Highlighting.tests createServer - scriptPreviewTests createServer - scriptEvictionTests createServer - scriptProjectOptionsCacheTests createServer - dependencyManagerTests createServer - interactiveDirectivesUnitTests - - // commented out because FSDN is down - //fsdnTest createServer - - //linterTests createServer - uriTests - formattingTests createServer - analyzerTests createServer - signatureTests createServer - SignatureHelp.tests createServer - CodeFixTests.Tests.tests createServer - Completion.tests createServer - GoTo.tests createServer - - FindReferences.tests createServer - Rename.tests createServer - - InfoPanelTests.docFormattingTest createServer - DetectUnitTests.tests createServer - XmlDocumentationGeneration.tests createServer - InlayHintTests.tests createServer - DependentFileChecking.tests createServer - UnusedDeclarationsTests.tests createServer - - ] ] - + for (sourceTextName, sourceTextFactory) in sourceTextFactories do + + testList + $"{loaderName}.{lspName}.{sourceTextName}" + [ + Templates.tests () + let createServer () = + lspFactory toolsPath workspaceLoaderFactory sourceTextFactory + + initTests createServer + closeTests createServer + + Utils.Tests.Server.tests createServer + Utils.Tests.CursorbasedTests.tests createServer + + CodeLens.tests createServer + documentSymbolTest createServer + Completion.autocompleteTest createServer + Completion.autoOpenTests createServer + foldingTests createServer + tooltipTests createServer + Highlighting.tests createServer + scriptPreviewTests createServer + scriptEvictionTests createServer + scriptProjectOptionsCacheTests createServer + dependencyManagerTests createServer + interactiveDirectivesUnitTests + + // commented out because FSDN is down + //fsdnTest createServer + + //linterTests createServer + uriTests + formattingTests createServer + analyzerTests createServer + signatureTests createServer + SignatureHelp.tests createServer + CodeFixTests.Tests.tests createServer + Completion.tests createServer + GoTo.tests createServer + + FindReferences.tests createServer + Rename.tests createServer + + InfoPanelTests.docFormattingTest createServer + DetectUnitTests.tests createServer + XmlDocumentationGeneration.tests createServer + InlayHintTests.tests createServer + DependentFileChecking.tests createServer + UnusedDeclarationsTests.tests createServer + + ] ] + /// Tests that do not require a LSP server let generalTests = testList "general" [ testList (nameof (Utils)) [ Utils.Tests.Utils.tests; Utils.Tests.TextEdit.tests ] - InlayHintTests.explicitTypeInfoTests - - FindReferences.tryFixupRangeTests + for (name, factory) in sourceTextFactories do + InlayHintTests.explicitTypeInfoTests (name, factory) + FindReferences.tryFixupRangeTests (name, factory) ] [] @@ -128,7 +136,7 @@ let tests = "FSAC" [ generalTests - lspTests + lspTests ] @@ -152,7 +160,7 @@ let main args = | Some "info" -> Logging.LogLevel.Info | Some "verbose" -> Logging.LogLevel.Verbose | Some "debug" -> Logging.LogLevel.Debug - | _ -> Logging.LogLevel.Info + | _ -> Logging.LogLevel.Warn let args = args