From 808891e695dceeaaf1e064ad7b98afb67f864796 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 May 2025 10:43:59 +0000 Subject: [PATCH 1/3] Initial plan for issue From cd6e58ea2324281de233c26db218ba6a085d95d5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 May 2025 10:48:37 +0000 Subject: [PATCH 2/3] Add in-memory source text support for Range.DebugCode Co-authored-by: vzarytovskii <1260985+vzarytovskii@users.noreply.github.com> --- src/Compiler/Utilities/range.fs | 68 ++++++++++++++++++++++++++------ src/Compiler/Utilities/range.fsi | 9 +++++ 2 files changed, 66 insertions(+), 11 deletions(-) diff --git a/src/Compiler/Utilities/range.fs b/src/Compiler/Utilities/range.fs index 5e13752df0b..ada668e4714 100755 --- a/src/Compiler/Utilities/range.fs +++ b/src/Compiler/Utilities/range.fs @@ -249,6 +249,25 @@ module FileIndex = // WARNING: Global Mutable State, holding a mapping between integers and filenames let fileIndexTable = FileIndexTable() + // ++GLOBAL MUTABLE STATE + // WARNING: Global Mutable State, holding a mapping between file paths and their in-memory source texts + let inMemorySourceTexts = ConcurrentDictionary() + + /// Register an in-memory source text for a given file path + let registerInMemorySourceText filePath text = + inMemorySourceTexts[filePath] <- text + filePath + + /// Try to get an in-memory source text for a given file path + let tryGetInMemorySourceText filePath = + match inMemorySourceTexts.TryGetValue filePath with + | true, text -> Some text + | _ -> None + + /// Clear all in-memory source texts + let clearInMemorySourceTexts() = + inMemorySourceTexts.Clear() + // If we exceed the maximum number of files we'll start to report incorrect file names let fileIndexOfFileAux normalize f = fileIndexTable.FileToIndex normalize f % maxFileIndex @@ -354,21 +373,48 @@ type Range(code1: int64, code2: int64) = then name else - try let endCol = m.EndColumn - 1 let startCol = m.StartColumn - 1 - if FileSystem.IsInvalidPathShim m.FileName then - "path invalid: " + m.FileName - elif not (FileSystem.FileExistsShim m.FileName) then - "nonexistent file: " + m.FileName - else - FileSystem.OpenFileForReadShim(m.FileName).ReadLines() - |> Seq.skip (m.StartLine - 1) - |> Seq.take (m.EndLine - m.StartLine + 1) - |> String.concat "\n" - |> fun s -> s.Substring(startCol + 1, s.LastIndexOf("\n", StringComparison.Ordinal) + 1 - startCol + endCol) + // Check if we have this source text in memory first + match tryGetInMemorySourceText m.FileName with + | Some sourceText -> + let lines = sourceText.Split('\n') + if m.StartLine > 0 && m.StartLine <= lines.Length then + let startLineIdx = m.StartLine - 1 + let endLineIdx = min (m.EndLine - 1) (lines.Length - 1) + let lineSlice = lines.[startLineIdx..endLineIdx] + let joined = String.concat "\n" lineSlice + + if joined.Length > 0 then + let effectiveStartCol = min startCol joined.Length + let substrLen = + if startLineIdx = endLineIdx then + min (endCol - startCol) (joined.Length - effectiveStartCol) + else + joined.Length - effectiveStartCol + + if substrLen > 0 then + joined.Substring(effectiveStartCol, substrLen) + else + joined + else + "empty source" + else + "line out of range in source" + + | None -> + if FileSystem.IsInvalidPathShim m.FileName then + "path invalid: " + m.FileName + elif not (FileSystem.FileExistsShim m.FileName) then + "nonexistent file: " + m.FileName + else + FileSystem.OpenFileForReadShim(m.FileName).ReadLines() + |> Seq.skip (m.StartLine - 1) + |> Seq.take (m.EndLine - m.StartLine + 1) + |> String.concat "\n" + |> fun s -> s.Substring(startCol + 1, s.LastIndexOf("\n", StringComparison.Ordinal) + 1 - startCol + endCol) with e -> e.ToString() diff --git a/src/Compiler/Utilities/range.fsi b/src/Compiler/Utilities/range.fsi index 90deb76d976..0ddedc9c92c 100755 --- a/src/Compiler/Utilities/range.fsi +++ b/src/Compiler/Utilities/range.fsi @@ -182,6 +182,15 @@ module internal FileIndex = val fileOfFileIndex: FileIndex -> string val startupFileName: string + + /// Register an in-memory source text for a given file path + val registerInMemorySourceText: filePath: string -> text: string -> string + + /// Try to get an in-memory source text for a given file path + val tryGetInMemorySourceText: filePath: string -> string option + + /// Clear all in-memory source texts + val clearInMemorySourceTexts: unit -> unit module Range = From a5119f6d29f781ebcc537d705e475eeff9a6d81f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 May 2025 10:56:27 +0000 Subject: [PATCH 3/3] Add tests for in-memory source text support Co-authored-by: vzarytovskii <1260985+vzarytovskii@users.noreply.github.com> --- .../CompilerService/InMemorySourceTests.fs | 69 +++++++++++++++++++ .../FSharp.Compiler.ComponentTests.fsproj | 1 + tests/FSharp.Compiler.Service.Tests/Common.fs | 12 ++++ 3 files changed, 82 insertions(+) create mode 100644 tests/FSharp.Compiler.ComponentTests/CompilerService/InMemorySourceTests.fs diff --git a/tests/FSharp.Compiler.ComponentTests/CompilerService/InMemorySourceTests.fs b/tests/FSharp.Compiler.ComponentTests/CompilerService/InMemorySourceTests.fs new file mode 100644 index 00000000000..dc7c96e56c6 --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/CompilerService/InMemorySourceTests.fs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace CompilerService + +open Xunit +open System +open FSharp.Test.Compiler +open FSharp.Compiler.Text + +module InMemorySourceTests = + + [] + let ``Range.DebugCode shows content from in-memory source text`` () = + // Test code that registers in-memory source and verifies DebugCode can see it + let testCode = """ +open System +open FSharp.Compiler.Text + +// Test case for in-memory source text with Range.DebugCode +try + // Clear any existing in-memory sources first + FileIndex.clearInMemorySourceTexts() + + // Register an in-memory source + let filePath = "test-in-memory.fs" + let sourceText = "let x = 42\nlet y = 87" + FileIndex.registerInMemorySourceText filePath sourceText |> ignore + + // Create a range for the in-memory source + let range = Range.mkRange filePath (Position.mkPos 1 4) (Position.mkPos 2 5) + + // Get the DebugCode representation using reflection to avoid debugger display limitations in tests + let debugText = + let property = typeof.GetProperty("DebugCode") + if isNull property then + failwith "DebugCode property not found on Range type" + let value = property.GetValue(range) :?> string + value + + // Verify the range debug text includes our source text content + if debugText.Contains("x = 42") |> not then + failwith $"Expected range debug text to contain source text, but got: {debugText}" + + // Clean up + FileIndex.clearInMemorySourceTexts() + + // Test for a non-existent file to verify it still shows the expected message + let nonExistentRange = Range.mkRange "nonexistent.fs" (Position.mkPos 1 0) (Position.mkPos 1 10) + let nonExistentDebugText = + let property = typeof.GetProperty("DebugCode") + if isNull property then + failwith "DebugCode property not found on Range type" + let value = property.GetValue(nonExistentRange) :?> string + value + + if nonExistentDebugText.Contains("nonexistent file:") |> not then + failwith $"Expected 'nonexistent file:' message for non-existent file, but got: {nonExistentDebugText}" + + printfn "Success: Range debug text contains source content" + true +with ex -> + printfn "Test failed: %s" ex.Message + false +""" + FSharp testCode + |> withReferenceFSharpCompilerService + |> asExe + |> compileAndRun + |> shouldSucceed \ No newline at end of file diff --git a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj index 36754f92217..27ef7cdf50c 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj +++ b/tests/FSharp.Compiler.ComponentTests/FSharp.Compiler.ComponentTests.fsproj @@ -307,6 +307,7 @@ + diff --git a/tests/FSharp.Compiler.Service.Tests/Common.fs b/tests/FSharp.Compiler.Service.Tests/Common.fs index f1de5739d47..69316c78a46 100644 --- a/tests/FSharp.Compiler.Service.Tests/Common.fs +++ b/tests/FSharp.Compiler.Service.Tests/Common.fs @@ -157,6 +157,10 @@ let parseAndCheckScriptWithOptions (file:string, input, opts) = let fname = Path.Combine(path, Path.GetFileName(file)) let dllName = Path.ChangeExtension(fname, ".dll") let projName = Path.ChangeExtension(fname, ".fsproj") + + // Register the source text with the range system for debugger display + FileIndex.registerInMemorySourceText file input |> ignore + let args = mkProjectCommandLineArgsForScript (dllName, []) printfn "file = %A, args = %A" file args checker.GetProjectOptionsFromCommandLineArgs (projName, args) @@ -167,6 +171,10 @@ let parseAndCheckScriptWithOptions (file:string, input, opts) = #else let projectOptions, _diagnostics = checker.GetProjectOptionsFromScript(file, SourceText.ofString input) |> Async.RunImmediate + + // Register the source text with the range system for debugger display + FileIndex.registerInMemorySourceText file input |> ignore + //printfn "projectOptions = %A" projectOptions #endif @@ -191,6 +199,10 @@ let parseSourceCode (name: string, code: string) = try Directory.CreateDirectory(location) |> ignore with _ -> () let filePath = Path.Combine(location, name) let dllPath = Path.Combine(location, name + ".dll") + + // Register the source text with the range system for debugger display + FileIndex.registerInMemorySourceText filePath code |> ignore + let args = mkProjectCommandLineArgs(dllPath, [filePath]) let options, _errors = checker.GetParsingOptionsFromCommandLineArgs(List.ofArray args) let parseResults = checker.ParseFile(filePath, SourceText.ofString code, options) |> Async.RunImmediate