From 2b9affccc09ae0afd3d7fbbae3f2cd150a37e318 Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Fri, 9 Oct 2026 17:19:32 +0200 Subject: [PATCH 1/3] Use the checker's document source for script snapshots GetProjectSnapshotFromScript fell back to the file system when no document source was passed, also when the checker was created with DocumentSource.Custom. The TransparentCompiler's GetProjectOptionsFromScript always passed DocumentSource.FileSystem. Both now use the document source of the checker. A file from a custom document source got DateTime.Now as its version, so every snapshot gave unchanged files a new version and the TransparentCompiler caches missed. CreateFromDocumentSource now gets the text when the snapshot is created and uses its checksum as the version. It returns Async for that reason. When the custom source returned None, getting the source failed with "Couldn't get source for file". The file is now read from disk, as the BackgroundCompiler does. See #20750. --- .../.FSharp.Compiler.Service/11.0.200.md | 1 + src/Compiler/Service/FSharpProjectSnapshot.fs | 42 +++++------ src/Compiler/Service/TransparentCompiler.fs | 9 +-- src/Compiler/Service/service.fs | 7 +- src/Compiler/Service/service.fsi | 2 +- .../FSharpChecker/TransparentCompiler.fs | 71 +++++++++++++++++++ ...iler.Service.SurfaceArea.netstandard20.bsl | 2 +- 7 files changed, 102 insertions(+), 32 deletions(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md index 4cd1275d7d6..228ded4064e 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md @@ -10,6 +10,7 @@ * `FsiEvaluationSession.EvalInteraction` and `EvalInteractionNonThrowing` evaluate every `;;`-separated interaction in the text they are given, as their documentation has always said, instead of silently discarding everything after the first. ([PR #20396](https://github.com/dotnet/fsharp/pull/20396)) * Prevent `InvalidProgramException` when runtime async awaits a `Task` upcast to `Task`, including through `ConfigureAwait`. ([Issue #20686](https://github.com/dotnet/fsharp/issues/20686), [PR #20726](https://github.com/dotnet/fsharp/pull/20726)) * Preserve constraint inference order when explicit generic arguments share a caller type parameter, avoiding spurious FS0001 errors with multiply implemented generic interfaces. ([PR #20491](https://github.com/dotnet/fsharp/pull/20491)) +* `GetProjectSnapshotFromScript` uses the document source of the checker when none is passed, also for the TransparentCompiler's `GetProjectOptionsFromScript`. A file from a `DocumentSource.Custom` keeps its snapshot version until its text changes, and a file for which the source returns `None` is read from disk. `FSharpFileSnapshot.CreateFromDocumentSource` now returns `Async`. ([Issue #20750](https://github.com/dotnet/fsharp/issues/20750)) * Build parser diagnostic messages without runtime printf specialization or reflective function construction, so Native AOT hosts report indentation and invalid-character diagnostics instead of failing during formatting. Preserve `#r` reference tooltips while script project options remain alive, including after garbage collection or cache eviction. ([PR #20658](https://github.com/dotnet/fsharp/pull/20658)) * Preserve tupled lambda argument groups when reducing state-machine applications, avoiding invalid argument substitutions and runtime failures with inline formatters. ([Issue #20675](https://github.com/dotnet/fsharp/issues/20675), [PR #20700](https://github.com/dotnet/fsharp/pull/20700)) * Preserve SRTP extension scopes across source files and competing witnesses through imported inline calls. Compile generic extension forwarding, including quotations, in unoptimized builds. Preserve applicable built-in operator priority in imported inline quotations. Do not reuse SRTP specializations across different selected witnesses. ([Issue #20683](https://github.com/dotnet/fsharp/issues/20683), [Issue #20684](https://github.com/dotnet/fsharp/issues/20684), [Issue #20685](https://github.com/dotnet/fsharp/issues/20685), [PR #20717](https://github.com/dotnet/fsharp/pull/20717)) diff --git a/src/Compiler/Service/FSharpProjectSnapshot.fs b/src/Compiler/Service/FSharpProjectSnapshot.fs index 02c368e7f46..82b783ed583 100644 --- a/src/Compiler/Service/FSharpProjectSnapshot.fs +++ b/src/Compiler/Service/FSharpProjectSnapshot.fs @@ -88,24 +88,21 @@ type FSharpFileSnapshot(FileName: string, Version: string, GetSource: unit -> Ta |> Task.FromResult ) - static member CreateFromDocumentSource(fileName: string, documentSource: DocumentSource) = - - match documentSource with - | DocumentSource.Custom f -> - let version = DateTime.Now.Ticks.ToString() - - FSharpFileSnapshot( - fileName, - version, - fun () -> - task { - match! f fileName |> Async.StartAsTask with - | Some source -> return SourceTextNew.ofISourceText source - | None -> return failwith $"Couldn't get source for file {fileName}" - } - ) - - | DocumentSource.FileSystem -> FSharpFileSnapshot.CreateFromFileSystem fileName + /// A custom source gets the file text now and uses its checksum as the version, + /// so a file that did not change keeps its version. When it returns None, the file is read from disk. + static member CreateFromDocumentSource(fileName: string, documentSource: DocumentSource) : Async = + async { + match documentSource with + | DocumentSource.Custom f -> + match! f fileName with + | Some source -> + let source = SourceTextNew.ofISourceText source + let version = source.GetChecksum().ToBuilder().ToArray() |> Md5Hasher.toString + return FSharpFileSnapshot(fileName, version, fun () -> Task.FromResult source) + | None -> return FSharpFileSnapshot.CreateFromFileSystem fileName + + | DocumentSource.FileSystem -> return FSharpFileSnapshot.CreateFromFileSystem fileName + } member public _.FileName = FileName member _.Version = Version @@ -713,12 +710,7 @@ and [] FSha } static member FromOptions(options: FSharpProjectOptions, documentSource: DocumentSource) = - FSharpProjectSnapshot.FromOptions( - options, - fun _ fileName -> - FSharpFileSnapshot.CreateFromDocumentSource(fileName, documentSource) - |> async.Return - ) + FSharpProjectSnapshot.FromOptions(options, fun _ fileName -> FSharpFileSnapshot.CreateFromDocumentSource(fileName, documentSource)) static member FromOptions (options: FSharpProjectOptions, fileName: string, fileVersion: int, sourceText: ISourceText, documentSource: DocumentSource) @@ -731,9 +723,9 @@ and [] FSha $"{fileVersion}{sourceText.GetHashCode().ToString()}", fun () -> Task.FromResult(SourceTextNew.ofISourceText sourceText) ) + |> async.Return else FSharpFileSnapshot.CreateFromDocumentSource(fName, documentSource) - |> async.Return FSharpProjectSnapshot.FromOptions(options, getFileSnapshot) diff --git a/src/Compiler/Service/TransparentCompiler.fs b/src/Compiler/Service/TransparentCompiler.fs index 8821c39aaca..4337e0aa6ea 100644 --- a/src/Compiler/Service/TransparentCompiler.fs +++ b/src/Compiler/Service/TransparentCompiler.fs @@ -2374,7 +2374,7 @@ type internal TransparentCompiler fileName, SourceTextNew.ofISourceText sourceText, caret, - DocumentSource.FileSystem, + documentSource, previewEnabled, loadedTimeStamp, otherFlags, @@ -2474,13 +2474,14 @@ type internal TransparentCompiler // Populate the cache. let! _ = caches.ScriptClosure.Get(loadClosureKey, async { return loadClosure }) - let sourceFiles = + let! sourceFiles = loadClosure.SourceFiles |> List.map (fun (sf, _) -> if sf = fileName then - currentSourceFile + async.Return currentSourceFile else FSharpFileSnapshot.CreateFromDocumentSource(sf, documentSource)) + |> MultipleDiagnosticsLoggers.Parallel let references = loadClosure.References @@ -2497,7 +2498,7 @@ type internal TransparentCompiler fileName + ".fsproj", None, None, - sourceFiles, + List.ofArray sourceFiles, references, otherFlags, List.empty, diff --git a/src/Compiler/Service/service.fs b/src/Compiler/Service/service.fs index a07f5f90e4e..282f241267e 100644 --- a/src/Compiler/Service/service.fs +++ b/src/Compiler/Service/service.fs @@ -530,7 +530,12 @@ type FSharpChecker ?userOpName: string ) = let userOpName = defaultArg userOpName "Unknown" - let documentSource = defaultArg documentSource DocumentSource.FileSystem + + let documentSource = + match documentSource, getSource with + | Some documentSource, _ -> documentSource + | None, Some getSource -> DocumentSource.Custom getSource + | None, None -> DocumentSource.FileSystem backgroundCompiler.GetProjectSnapshotFromScript( fileName, diff --git a/src/Compiler/Service/service.fsi b/src/Compiler/Service/service.fsi index 40410c4a004..cb9aa5af86e 100644 --- a/src/Compiler/Service/service.fsi +++ b/src/Compiler/Service/service.fsi @@ -254,7 +254,7 @@ type public FSharpChecker = /// Used to differentiate between scripts, to consider each script a separate project. Also used in formatted error messages. /// The source for the file. /// The editor location for the cursor if available. - /// DocumentSource to load any additional files. + /// DocumentSource to load any additional files. Defaults to the document source of the checker. /// Is the preview compiler enabled. /// Indicates when the script was loaded into the editing environment, /// so that an 'unload' and 'reload' action will cause the script to be considered as a new project, diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs index fceb69f8f19..9de9718736a 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs @@ -1214,6 +1214,77 @@ module TestsMutatingFileSystem = FileSystemAutoOpens.FileSystem <- currentFileSystem } +/// Writes a.fsx, which loads b.fsx, to disk and returns both paths. +let private createScriptThatLoadsFile () = + let directory = TestFramework.createTemporaryDirectory () + let a = Path.Combine(directory.FullName, "a.fsx") + let b = Path.Combine(directory.FullName, "b.fsx") + File.WriteAllText(a, "#load \"b.fsx\"") + File.WriteAllText(b, "let onDisk = 1") + a, b + +let private getLoadedFile (checker: FSharpChecker) (documentSource: DocumentSource option) (script: string) (loadedFile: string) = + async { + let! snapshot, _ = + checker.GetProjectSnapshotFromScript(script, SourceTextNew.ofString (File.ReadAllText script), ?documentSource = documentSource) + return snapshot.SourceFiles |> List.find (fun file -> file.FileName = loadedFile) + } + +let private getText (file: ProjectSnapshot.FSharpFileSnapshot) = + task { + let! source = file.GetSource() + return source.GetSubTextString(0, source.Length) + } + +[] +[] +[] +let ``GetProjectSnapshotFromScript uses the document source of the checker`` useTransparentCompiler = + task { + let a, b = createScriptThatLoadsFile () + let getSource fileName = async { return if fileName = b then Some(SourceText.ofString "let inEditor = 1") else None } + let checker = FSharpChecker.Create(useTransparentCompiler = useTransparentCompiler, documentSource = DocumentSource.Custom getSource) + + let! loadedFile = getLoadedFile checker None a b + let! text = getText loadedFile + Assert.Equal("let inEditor = 1", text) + } + +[] +[] +[] +let ``A custom document source gives a file the same version until its text changes`` useTransparentCompiler = + task { + let a, b = createScriptThatLoadsFile () + let mutable textOfB = "let inEditor = 1" + let getSource fileName = async { return if fileName = b then Some(SourceText.ofString textOfB) else None } + let documentSource = Some(DocumentSource.Custom getSource) + let checker = FSharpChecker.Create(useTransparentCompiler = useTransparentCompiler) + + let! first = getLoadedFile checker documentSource a b + let! second = getLoadedFile checker documentSource a b + Assert.Equal(first.Version, second.Version) + + textOfB <- "let inEditor = 2" + let! third = getLoadedFile checker documentSource a b + Assert.NotEqual(first.Version, third.Version) + } + +[] +[] +[] +let ``A custom document source that returns None reads the file from disk`` useTransparentCompiler = + task { + let a, b = createScriptThatLoadsFile () + let getSource (_: string) = async { return None } + let documentSource = Some(DocumentSource.Custom getSource) + let checker = FSharpChecker.Create(useTransparentCompiler = useTransparentCompiler) + + let! loadedFile = getLoadedFile checker documentSource a b + let! text = getText loadedFile + Assert.Equal("let onDisk = 1", text) + } + [] let ``Parsing without cache and without project snapshot`` () = async { diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl index b65bfa60260..bdc5a226bcd 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl @@ -2452,10 +2452,10 @@ FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: Boolean Equals( FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: Boolean IsSignatureFile FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: Boolean get_IsSignatureFile() FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: FSharpFileSnapshot Create(System.String, System.String, Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,System.Threading.Tasks.Task`1[FSharp.Compiler.Text.ISourceTextNew]]) -FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: FSharpFileSnapshot CreateFromDocumentSource(System.String, FSharp.Compiler.CodeAnalysis.DocumentSource) FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: FSharpFileSnapshot CreateFromFileSystem(System.String) FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: FSharpFileSnapshot CreateFromString(System.String, System.String) FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: Int32 GetHashCode() +FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: Microsoft.FSharp.Control.FSharpAsync`1[FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot] CreateFromDocumentSource(System.String, FSharp.Compiler.CodeAnalysis.DocumentSource) FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: System.String FileName FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: System.String GetFileName() FSharp.Compiler.CodeAnalysis.ProjectSnapshot+FSharpFileSnapshot: System.String Version From f2a30ee06fc041c624ff6656bde7c63f837c1f77 Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Fri, 9 Oct 2026 17:23:07 +0200 Subject: [PATCH 2/3] Add PR link to release notes entry --- docs/release-notes/.FSharp.Compiler.Service/11.0.200.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md index 228ded4064e..11791d1df49 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md @@ -10,7 +10,7 @@ * `FsiEvaluationSession.EvalInteraction` and `EvalInteractionNonThrowing` evaluate every `;;`-separated interaction in the text they are given, as their documentation has always said, instead of silently discarding everything after the first. ([PR #20396](https://github.com/dotnet/fsharp/pull/20396)) * Prevent `InvalidProgramException` when runtime async awaits a `Task` upcast to `Task`, including through `ConfigureAwait`. ([Issue #20686](https://github.com/dotnet/fsharp/issues/20686), [PR #20726](https://github.com/dotnet/fsharp/pull/20726)) * Preserve constraint inference order when explicit generic arguments share a caller type parameter, avoiding spurious FS0001 errors with multiply implemented generic interfaces. ([PR #20491](https://github.com/dotnet/fsharp/pull/20491)) -* `GetProjectSnapshotFromScript` uses the document source of the checker when none is passed, also for the TransparentCompiler's `GetProjectOptionsFromScript`. A file from a `DocumentSource.Custom` keeps its snapshot version until its text changes, and a file for which the source returns `None` is read from disk. `FSharpFileSnapshot.CreateFromDocumentSource` now returns `Async`. ([Issue #20750](https://github.com/dotnet/fsharp/issues/20750)) +* `GetProjectSnapshotFromScript` uses the document source of the checker when none is passed, also for the TransparentCompiler's `GetProjectOptionsFromScript`. A file from a `DocumentSource.Custom` keeps its snapshot version until its text changes, and a file for which the source returns `None` is read from disk. `FSharpFileSnapshot.CreateFromDocumentSource` now returns `Async`. ([Issue #20750](https://github.com/dotnet/fsharp/issues/20750), [PR #20751](https://github.com/dotnet/fsharp/pull/20751)) * Build parser diagnostic messages without runtime printf specialization or reflective function construction, so Native AOT hosts report indentation and invalid-character diagnostics instead of failing during formatting. Preserve `#r` reference tooltips while script project options remain alive, including after garbage collection or cache eviction. ([PR #20658](https://github.com/dotnet/fsharp/pull/20658)) * Preserve tupled lambda argument groups when reducing state-machine applications, avoiding invalid argument substitutions and runtime failures with inline formatters. ([Issue #20675](https://github.com/dotnet/fsharp/issues/20675), [PR #20700](https://github.com/dotnet/fsharp/pull/20700)) * Preserve SRTP extension scopes across source files and competing witnesses through imported inline calls. Compile generic extension forwarding, including quotations, in unoptimized builds. Preserve applicable built-in operator priority in imported inline quotations. Do not reuse SRTP specializations across different selected witnesses. ([Issue #20683](https://github.com/dotnet/fsharp/issues/20683), [Issue #20684](https://github.com/dotnet/fsharp/issues/20684), [Issue #20685](https://github.com/dotnet/fsharp/issues/20685), [PR #20717](https://github.com/dotnet/fsharp/pull/20717)) From 7a64d107ef42ff0d3d46589b80c5720afca6f727 Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Mon, 12 Oct 2026 13:14:44 +0200 Subject: [PATCH 3/3] Compute the checksum of a wrapped ISourceText from its text SourceTextNew.ofISourceText hashed sourceText.ToString() for the checksum. For an ISourceText that does not override ToString, or for an ISourceTextNew wrapped again, every text gave the same checksum. Since CreateFromDocumentSource now uses that checksum as the snapshot version, a changed file kept its version. An ISourceTextNew is now returned unchanged, so its own checksum is used. Any other ISourceText is hashed from its text. --- .../.FSharp.Compiler.Service/11.0.200.md | 2 +- src/Compiler/Facilities/prim-lexing.fs | 38 ++++++++++--------- .../FSharpChecker/TransparentCompiler.fs | 26 +++++++++++-- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md index 11791d1df49..ced5ffc8331 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md @@ -10,7 +10,7 @@ * `FsiEvaluationSession.EvalInteraction` and `EvalInteractionNonThrowing` evaluate every `;;`-separated interaction in the text they are given, as their documentation has always said, instead of silently discarding everything after the first. ([PR #20396](https://github.com/dotnet/fsharp/pull/20396)) * Prevent `InvalidProgramException` when runtime async awaits a `Task` upcast to `Task`, including through `ConfigureAwait`. ([Issue #20686](https://github.com/dotnet/fsharp/issues/20686), [PR #20726](https://github.com/dotnet/fsharp/pull/20726)) * Preserve constraint inference order when explicit generic arguments share a caller type parameter, avoiding spurious FS0001 errors with multiply implemented generic interfaces. ([PR #20491](https://github.com/dotnet/fsharp/pull/20491)) -* `GetProjectSnapshotFromScript` uses the document source of the checker when none is passed, also for the TransparentCompiler's `GetProjectOptionsFromScript`. A file from a `DocumentSource.Custom` keeps its snapshot version until its text changes, and a file for which the source returns `None` is read from disk. `FSharpFileSnapshot.CreateFromDocumentSource` now returns `Async`. ([Issue #20750](https://github.com/dotnet/fsharp/issues/20750), [PR #20751](https://github.com/dotnet/fsharp/pull/20751)) +* `GetProjectSnapshotFromScript` uses the document source of the checker when none is passed, also for the TransparentCompiler's `GetProjectOptionsFromScript`. A file from a `DocumentSource.Custom` keeps its snapshot version until its text changes, and a file for which the source returns `None` is read from disk. `FSharpFileSnapshot.CreateFromDocumentSource` now returns `Async`. `SourceTextNew.ofISourceText` returns an `ISourceTextNew` unchanged, and otherwise computes the checksum from the text instead of from `ToString()`. ([Issue #20750](https://github.com/dotnet/fsharp/issues/20750), [PR #20751](https://github.com/dotnet/fsharp/pull/20751)) * Build parser diagnostic messages without runtime printf specialization or reflective function construction, so Native AOT hosts report indentation and invalid-character diagnostics instead of failing during formatting. Preserve `#r` reference tooltips while script project options remain alive, including after garbage collection or cache eviction. ([PR #20658](https://github.com/dotnet/fsharp/pull/20658)) * Preserve tupled lambda argument groups when reducing state-machine applications, avoiding invalid argument substitutions and runtime failures with inline formatters. ([Issue #20675](https://github.com/dotnet/fsharp/issues/20675), [PR #20700](https://github.com/dotnet/fsharp/pull/20700)) * Preserve SRTP extension scopes across source files and competing witnesses through imported inline calls. Compile generic extension forwarding, including quotations, in unoptimized builds. Preserve applicable built-in operator priority in imported inline quotations. Do not reuse SRTP specializations across different selected witnesses. ([Issue #20683](https://github.com/dotnet/fsharp/issues/20683), [Issue #20684](https://github.com/dotnet/fsharp/issues/20684), [Issue #20685](https://github.com/dotnet/fsharp/issues/20685), [PR #20717](https://github.com/dotnet/fsharp/pull/20717)) diff --git a/src/Compiler/Facilities/prim-lexing.fs b/src/Compiler/Facilities/prim-lexing.fs index 21b93b12880..33e924d3f1c 100644 --- a/src/Compiler/Facilities/prim-lexing.fs +++ b/src/Compiler/Facilities/prim-lexing.fs @@ -167,34 +167,36 @@ module SourceTextNew = let ofString str = StringText(str) :> ISourceTextNew let ofISourceText (sourceText: ISourceText) = - { new ISourceTextNew with - member _.Item - with get index = sourceText[index] + match sourceText with + | :? ISourceTextNew as sourceText -> sourceText + | _ -> + { new ISourceTextNew with + member _.Item + with get index = sourceText[index] - member _.GetLineString(x) = sourceText.GetLineString(x) + member _.GetLineString(x) = sourceText.GetLineString(x) - member _.GetLineCount() = sourceText.GetLineCount() + member _.GetLineCount() = sourceText.GetLineCount() - member _.GetLastCharacterPosition() = sourceText.GetLastCharacterPosition() + member _.GetLastCharacterPosition() = sourceText.GetLastCharacterPosition() - member _.GetSubTextString(x, y) = sourceText.GetSubTextString(x, y) + member _.GetSubTextString(x, y) = sourceText.GetSubTextString(x, y) - member _.SubTextEquals(x, y) = sourceText.SubTextEquals(x, y) + member _.SubTextEquals(x, y) = sourceText.SubTextEquals(x, y) - member _.Length = sourceText.Length + member _.Length = sourceText.Length - member _.ContentEquals(x) = sourceText.ContentEquals(x) + member _.ContentEquals(x) = sourceText.ContentEquals(x) - member _.CopyTo(a, b, c, d) = sourceText.CopyTo(a, b, c, d) + member _.CopyTo(a, b, c, d) = sourceText.CopyTo(a, b, c, d) - member _.GetSubTextFromRange(x) = sourceText.GetSubTextFromRange(x) + member _.GetSubTextFromRange(x) = sourceText.GetSubTextFromRange(x) - member _.GetChecksum() = - // TODO: something better... - !!sourceText.ToString() - |> Md5Hasher.hashString - |> fun byteArray -> ImmutableArray.Create(byteArray, 0, byteArray.Length) - } + member _.GetChecksum() = + sourceText.GetSubTextString(0, sourceText.Length) + |> Md5Hasher.hashString + |> fun byteArray -> ImmutableArray.Create(byteArray, 0, byteArray.Length) + } // NOTE: the code in this file is a drop-in replacement runtime for Lexing.fs from the FsLexYacc repository diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs index 9de9718736a..2769edb2a2f 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs @@ -1236,6 +1236,21 @@ let private getText (file: ProjectSnapshot.FSharpFileSnapshot) = return source.GetSubTextString(0, source.Length) } +/// An ISourceText that is not an ISourceTextNew and does not override ToString. +let private plainSourceText (text: string) = + let inner = SourceText.ofString text + { new ISourceText with + member _.Item with get index = inner[index] + member _.GetLineString(lineIndex) = inner.GetLineString(lineIndex) + member _.GetLineCount() = inner.GetLineCount() + member _.GetLastCharacterPosition() = inner.GetLastCharacterPosition() + member _.GetSubTextString(start, length) = inner.GetSubTextString(start, length) + member _.SubTextEquals(target, startIndex) = inner.SubTextEquals(target, startIndex) + member _.Length = inner.Length + member _.ContentEquals(sourceText) = inner.ContentEquals(sourceText) + member _.CopyTo(sourceIndex, destination, destinationIndex, count) = inner.CopyTo(sourceIndex, destination, destinationIndex, count) + member _.GetSubTextFromRange(range) = inner.GetSubTextFromRange(range) } + [] [] [] @@ -1251,13 +1266,16 @@ let ``GetProjectSnapshotFromScript uses the document source of the checker`` use } [] -[] -[] -let ``A custom document source gives a file the same version until its text changes`` useTransparentCompiler = +[] +[] +[] +[] +let ``A custom document source gives a file the same version until its text changes`` useTransparentCompiler usePlainSourceText = task { let a, b = createScriptThatLoadsFile () let mutable textOfB = "let inEditor = 1" - let getSource fileName = async { return if fileName = b then Some(SourceText.ofString textOfB) else None } + let toSourceText = if usePlainSourceText then plainSourceText else SourceText.ofString + let getSource fileName = async { return if fileName = b then Some(toSourceText textOfB) else None } let documentSource = Some(DocumentSource.Custom getSource) let checker = FSharpChecker.Create(useTransparentCompiler = useTransparentCompiler)