diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index 85e664921..ebb226282 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -114,6 +114,8 @@ type AdaptiveWorkspaceChosen = + + [] type LoadedProject = { ProjectOptions: Types.ProjectOptions @@ -1280,20 +1282,22 @@ type AdaptiveState /// Parses a source code for a file and caches the results. Returns an AST that can be traversed for various features. /// The FSharpCompilerServiceChecker. - /// The source to be parsed. + /// The source to be parsed. /// /// - let parseFile (checker: FSharpCompilerServiceChecker) (sourceFilePath) (compilerOptions: CompilerProjectOption) = + + let parseFile (checker: FSharpCompilerServiceChecker) (file: VolatileFile) (compilerOptions: CompilerProjectOption) = task { let! result = match compilerOptions with | CompilerProjectOption.TransparentCompiler snap -> - taskResult { return! checker.ParseFile(sourceFilePath, snap) } + taskResult { return! checker.ParseFile(file.FileName, snap) } | CompilerProjectOption.BackgroundCompiler opts -> taskResult { - let! file = forceFindOpenFileOrRead sourceFilePath - return! checker.ParseFile(sourceFilePath, file.Source, opts) + + + return! checker.ParseFile(file.FileName, file.Source, opts) } let! ct = Async.CancellationToken @@ -1324,8 +1328,12 @@ type AdaptiveState |> HashSet.toArray |> Array.collect (fun (snap) -> snap.SourceFilesTagged |> List.toArray |> Array.map (fun s -> snap, s)) |> Array.map (fun (snap, filePath) -> + taskResult { + let! vFile = forceFindOpenFileOrRead filePath + return! parseFile checker vFile snap + + }) - parseFile checker filePath snap) |> Task.WhenAll } @@ -1435,14 +1443,19 @@ type AdaptiveState let allFSharpFilesAndProjectOptions = asyncAVal { - let wins = - openFilesToChangesAndProjectOptions - |> AMap.map (fun _k v -> v |> AsyncAVal.mapSync (fun (_, projects) _ -> projects)) + let wins = openFilesToChangesAndProjectOptions let! sourceFileToProjectOptions = sourceFileToProjectOptions let loses = - sourceFileToProjectOptions |> AMap.map (fun _ v -> AsyncAVal.constant (Ok v)) + sourceFileToProjectOptions + |> AMap.map (fun file proj -> + asyncAVal { + let! lastTouched = AdaptiveFile.GetLastWriteTimeUtc(UMX.untag file) + let! vFile = createVolatileFileFromDisk lastTouched file + + return vFile, Ok proj + }) return AMap.union loses wins } @@ -1462,7 +1475,7 @@ type AdaptiveState return allFSharpFilesAndProjectOptions - |> AMapAsync.mapAsyncAVal (fun filePath (options: Result) _ctok -> + |> AMapAsync.mapAsyncAVal (fun filePath (file, options) _ctok -> asyncAVal { let! (checker: FSharpCompilerServiceChecker) = checker and! selectProject = projectSelector @@ -1473,7 +1486,7 @@ type AdaptiveState match loadedProject with | Ok x -> let! snap = x.FSharpProjectCompilerOptions - let! r = parseFile checker filePath snap + let! r = parseFile checker file snap return r | Error e -> return Error e }) @@ -1505,7 +1518,7 @@ type AdaptiveState return set - |> Array.choose (fun (k, v) -> + |> Array.choose (fun (k, (_, v)) -> v |> Result.bind (findProject k) |> Result.toOption @@ -1524,7 +1537,7 @@ type AdaptiveState |> Array.map (AsyncAVal.forceAsync) |> Async.parallel75 - let set = set |> Array.choose (Result.toOption) + let set = set |> Array.choose (snd >> Result.toOption) return set |> Array.collect (List.toArray) } @@ -1539,7 +1552,7 @@ type AdaptiveState let! allFilesToFSharpProjectOptions = allFilesToFSharpProjectOptions match! allFilesToFSharpProjectOptions |> AMapAsync.tryFindA filePath with - | Some projs -> return projs + | Some(_, projs) -> return projs | None -> return Error $"Couldn't find project for {filePath}. Have you tried restoring your project/solution?" } diff --git a/test/FsAutoComplete.Tests.Lsp/CodeLensTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeLensTests.fs index 5dfe64b8f..8d77dd715 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeLensTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeLensTests.fs @@ -1,5 +1,6 @@ module FsAutoComplete.Tests.CodeLens +open FsAutoComplete.Utils open Expecto open FsToolkit.ErrorHandling open Helpers @@ -12,119 +13,215 @@ open Utils.CursorbasedTests open Utils.Tests.TextEdit open Newtonsoft.Json.Linq open Helpers.Expecto.ShadowedTimeouts +open System.IO module private CodeLens = + let examples = Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "CodeLensProjectTests") + + module CodeLensPositionStaysAccurate = + let dir = examples "CodeLens_position_stays_accurate" + let project = dir "CodeLens_position_stays_accurate.fsproj" + let programFile = dir "Program.fs" + let assertNoDiagnostics (ds: Diagnostic[]) = match ds with | [||] -> Ok() | ds -> Error $"Expected no diagnostics, but got %A{ds}" - let check server text checkLenses = + let getLenses (doc: Document) = + let p: CodeLensParams = + { TextDocument = doc.TextDocumentIdentifier + PartialResultToken = None + WorkDoneToken = None } + + doc.Server.Server.TextDocumentCodeLens p + |> AsyncResult.mapError string + |> AsyncResult.map (fun r -> r |> Option.defaultValue [||]) + + let getResolvedLenses (doc: Document) lenses = + lenses + |> List.ofArray + |> List.traverseAsyncResultA doc.Server.Server.CodeLensResolve + |> AsyncResult.mapError string + + let check server text (checkLenses: Document * CodeLens list * CodeLens array * CodeLens list -> Async) = asyncResult { let textRange, text = text |> Text.trimTripleQuotation |> Cursor.assertExtractRange let! (doc, diags) = Server.createUntitledDocument text server do! assertNoDiagnostics diags - let p: CodeLensParams = - { TextDocument = doc.TextDocumentIdentifier - PartialResultToken = None - WorkDoneToken = None } - - let! lenses = doc.Server.Server.TextDocumentCodeLens p |> AsyncResult.mapError string - - let! resolved = - Option.toList lenses - |> Array.concat - |> List.ofArray - |> List.traverseAsyncResultA doc.Server.Server.CodeLensResolve - |> AsyncResult.mapError string + let! lenses = getLenses doc + let! resolved = getResolvedLenses doc lenses let lensesForRange = resolved |> List.filter (fun lens -> Range.overlapsStrictly textRange lens.Range) - checkLenses (doc, lensesForRange) + do! checkLenses (doc, lensesForRange, lenses, resolved) } |> AsyncResult.foldResult id (fun e -> failtest $"{e}") +let projectBasedTests state = + testList "ProjectBased" [ + serverTestList ("CodeLensPositionStaysAccurate") state defaultConfigDto (Some CodeLens.CodeLensPositionStaysAccurate.dir) (fun server -> [ + + testCaseAsync "can show codelens after adding newlines to code" + <| (asyncResult { + let program = CodeLens.CodeLensPositionStaysAccurate.programFile + let! (doc, _diags) = Server.openDocument program server + + let! unresolved = CodeLens.getLenses doc + let! resolved = CodeLens.getResolvedLenses doc unresolved + + let references = + resolved + |> List.filter (fun lens -> lens.Command |>Option.exists (fun c -> c.Title.EndsWith "References")) + |> List.sortBy (fun lens -> lens.Range.Start.Line) + + Expect.hasLength references 2 "should have a reference lens" + + let lens1 = references.[0] + let lens1Range : Range = { + Start = { Line = 1u; Character = 6u } + End = { Line = 1u; Character = 20u } + } + + Expect.equal lens1.Range lens1Range "Lens 1 should be at 1:6-1:20" + + let lens2 = references.[1] + let lens2Range : Range = { + Start = { Line = 3u; Character = 6u } + End = { Line = 3u; Character = 25u } + } + + Expect.equal lens2.Range lens2Range "Lens 2 should be at 3:6-3:25" + + do! doc.Server.Server.TextDocumentDidChange({ + TextDocument = doc.VersionedTextDocumentIdentifier + ContentChanges = [| U2.C1 { + Range = { Start = { Line = 2u; Character = 0u }; End = { Line = 2u; Character = 0u }; } + RangeLength = None + Text = "\n\n" + } |] + }) + + let! nextLens = CodeLens.getLenses doc + let! resolvedNextLens = CodeLens.getResolvedLenses doc nextLens + + let references = + resolvedNextLens + |> List.filter (fun lens -> lens.Command |>Option.exists (fun c -> c.Title.EndsWith "References")) + |> List.sortBy (fun lens -> lens.Range.Start.Line) + + let lens1 = references.[0] + let lens1Range : Range = { + Start = { Line = 1u; Character = 6u } + End = { Line = 1u; Character = 20u } + } + + Expect.equal lens1.Range lens1Range "Lens 1 should be at 1:6-1:20" + + let lens2 = references.[1] + let lens2Range : Range = { + Start = { Line = 5u; Character = 6u } + End = { Line = 5u; Character = 25u } + } + + Expect.equal lens2.Range lens2Range "Lens 2 should be at 5:6-5:25" + + return () + } + |> AsyncResult.foldResult id (fun e -> failtest $"{e}" )) + + ] + ) + ] + let tests state = - serverTestList (nameof CodeLens) state defaultConfigDto None (fun server -> - [ testCaseAsync "can show codelens for type annotation" - <| CodeLens.check server """ - module X = - $0let func x = x + 1$0 - """ (fun (_doc, lenses) -> - Expect.hasLength lenses 2 "should have a type lens and a reference lens" - let typeLens = lenses[0] - Expect.equal typeLens.Command.Value.Title "int -> int" "first lens should be a type hint of int to int" - Expect.isNone typeLens.Command.Value.Arguments "No data required for type lenses" - Expect.equal typeLens.Command.Value.Command "" "No command for type lenses") - - testCaseAsync "can show codelens for 0 reference count" - <| CodeLens.check server """ - module X = - $0let func x = x + 1$0 - """ (fun (_doc, lenses) -> - Expect.hasLength lenses 2 "should have a type lens and a reference lens" - let referenceLens = lenses[1] - - let emptyCommand = - Some - { Title = "0 References" - Arguments = None - Command = "" } - - Expect.equal referenceLens.Command emptyCommand "There should be no command or args for zero references") - testCaseAsync "can show codelens for multi reference count" - <| CodeLens.check server """ - module X = - $0let func x = x + 1$0 - - let doThing () = func 1 - """ (fun (doc, lenses) -> - Expect.hasLength lenses 2 "should have a type lens and a reference lens" - let referenceLens = lenses[1] - Expect.isSome referenceLens.Command "There should be a command for multiple references" - let referenceCommand = referenceLens.Command.Value - Expect.equal referenceCommand.Title "1 References" "There should be a title for multiple references" - - Expect.equal - referenceCommand.Command - "fsharp.showReferences" - "There should be a command for multiple references" - - Expect.isSome referenceCommand.Arguments "There should be arguments for multiple references" - let args = referenceCommand.Arguments.Value - Expect.equal args.Length 3 "There should be 2 args" - - let filePath, triggerPos, referenceRanges = - args[0].Value(), - (args[1] :?> JObject).ToObject(), - (args[2] :?> JArray) - |> Seq.map (fun t -> (t :?> JObject).ToObject()) - |> Array.ofSeq - - Expect.equal filePath doc.Uri "File path should be the doc we're checking" - Expect.equal triggerPos { Line = 1u; Character = 6u } "Position should be 1:6" - Expect.hasLength referenceRanges 1 "There should be 1 reference range for the `func` function" - - Expect.equal - referenceRanges[0] - { Uri = doc.Uri - Range = - { Start = { Line = 3u; Character = 19u } - End = { Line = 3u; Character = 23u } } } - "Reference range should be 0:0") - testCaseAsync "can show reference counts for 1-character identifier" - <| CodeLens.check server """ - $0let f () = ""$0 - """ (fun (_doc, lenses) -> - Expect.hasLength lenses 2 "should have a type lens and a reference lens" - let referenceLens = lenses[1] - Expect.isSome referenceLens.Command "There should be a command for multiple references" - let referenceCommand = referenceLens.Command.Value - Expect.equal referenceCommand.Title "0 References" "There should be a title for multiple references" - Expect.equal referenceCommand.Command "" "There should be no command for multiple references" - Expect.isNone referenceCommand.Arguments "There should be arguments for multiple references") ]) + + testList (nameof CodeLens) [ + projectBasedTests state + serverTestList "scriptTests" state defaultConfigDto None (fun server -> + [ testCaseAsync "can show codelens for type annotation" + <| CodeLens.check server """ + module X = + $0let func x = x + 1$0 + """ (fun (_doc, lenses, _unresolved, _resolved) -> async { + Expect.hasLength lenses 2 "should have a type lens and a reference lens" + let typeLens = lenses[0] + Expect.equal typeLens.Command.Value.Title "int -> int" "first lens should be a type hint of int to int" + Expect.isNone typeLens.Command.Value.Arguments "No data required for type lenses" + Expect.equal typeLens.Command.Value.Command "" "No command for type lenses" }) + + testCaseAsync "can show codelens for 0 reference count" + <| CodeLens.check server """ + module X = + $0let func x = x + 1$0 + """ (fun (_doc, lenses, _unresolved, _resolved) -> async { + Expect.hasLength lenses 2 "should have a type lens and a reference lens" + let referenceLens = lenses[1] + + let emptyCommand = + Some + { Title = "0 References" + Arguments = None + Command = "" } + + Expect.equal referenceLens.Command emptyCommand "There should be no command or args for zero references" }) + testCaseAsync "can show codelens for multi reference count" + <| CodeLens.check server """ + module X = + $0let func x = x + 1$0 + + let doThing () = func 1 + """ (fun (doc, lenses, _unresolved, _resolved) -> async { + + + Expect.hasLength lenses 2 "should have a type lens and a reference lens" + let referenceLens = lenses[1] + Expect.isSome referenceLens.Command "There should be a command for multiple references" + let referenceCommand = referenceLens.Command.Value + Expect.equal referenceCommand.Title "1 References" "There should be a title for multiple references" + + Expect.equal + referenceCommand.Command + "fsharp.showReferences" + "There should be a command for multiple references" + + Expect.isSome referenceCommand.Arguments "There should be arguments for multiple references" + let args = referenceCommand.Arguments.Value + Expect.equal args.Length 3 "There should be 2 args" + + let filePath, triggerPos, referenceRanges = + args[0].Value(), + (args[1] :?> JObject).ToObject(), + (args[2] :?> JArray) + |> Seq.map (fun t -> (t :?> JObject).ToObject()) + |> Array.ofSeq + + Expect.equal filePath doc.Uri "File path should be the doc we're checking" + Expect.equal triggerPos { Line = 1u; Character = 6u } "Position should be 1:6" + Expect.hasLength referenceRanges 1 "There should be 1 reference range for the `func` function" + + Expect.equal + referenceRanges[0] + { Uri = doc.Uri + Range = + { Start = { Line = 3u; Character = 19u } + End = { Line = 3u; Character = 23u } } } + "Reference range should be 0:0"}) + testCaseAsync "can show reference counts for 1-character identifier" + <| CodeLens.check server """ + $0let f () = ""$0 + """ (fun (_doc, lenses, _unresolved, _resolved) -> async { + Expect.hasLength lenses 2 "should have a type lens and a reference lens" + let referenceLens = lenses[1] + Expect.isSome referenceLens.Command "There should be a command for multiple references" + let referenceCommand = referenceLens.Command.Value + Expect.equal referenceCommand.Title "0 References" "There should be a title for multiple references" + Expect.equal referenceCommand.Command "" "There should be no command for multiple references" + Expect.isNone referenceCommand.Arguments "There should be arguments for multiple references"}) ]) + + ] diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/CodeLensProjectTests/CodeLens_position_stays_accurate/CodeLens_position_stays_accurate.fsproj b/test/FsAutoComplete.Tests.Lsp/TestCases/CodeLensProjectTests/CodeLens_position_stays_accurate/CodeLens_position_stays_accurate.fsproj new file mode 100644 index 000000000..5a3c69724 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/CodeLensProjectTests/CodeLens_position_stays_accurate/CodeLens_position_stays_accurate.fsproj @@ -0,0 +1,12 @@ + + + + Exe + net8.0 + + + + + + + diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/CodeLensProjectTests/CodeLens_position_stays_accurate/Program.fs b/test/FsAutoComplete.Tests.Lsp/TestCases/CodeLensProjectTests/CodeLens_position_stays_accurate/Program.fs new file mode 100644 index 000000000..b91a9ddd5 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/CodeLensProjectTests/CodeLens_position_stays_accurate/Program.fs @@ -0,0 +1,4 @@ +module X = + let func x = x + 1 + + let doThing () = func 1