Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 57 additions & 11 deletions src/Compiler/Utilities/range.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>()

/// 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
Expand Down Expand Up @@ -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()

Expand Down
9 changes: 9 additions & 0 deletions src/Compiler/Utilities/range.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -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 =

Expand Down
Original file line number Diff line number Diff line change
@@ -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 =

[<Fact>]
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<Range>.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<Range>.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
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@
<Compile Include="CompilerOptions\fsc\refonlyrefout.fs" />
<Compile Include="CompilerOptions\fsc\sourceFiles.fs" />
<Compile Include="CompilerService\RangeModule.fs" />
<Compile Include="CompilerService\InMemorySourceTests.fs" />
<Compile Include="CompilerService\Caches.fs" />
<Compile Include="CompilerService\LruCache.fs" />
<Compile Include="CompilerService\AsyncMemoize.fs" />
Expand Down
12 changes: 12 additions & 0 deletions tests/FSharp.Compiler.Service.Tests/Common.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand All @@ -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
Expand Down
Loading