diff --git a/src/FsAutoComplete.Core/AbstractClassStubGenerator.fs b/src/FsAutoComplete.Core/AbstractClassStubGenerator.fs index e64c1b0d7..6d60af652 100644 --- a/src/FsAutoComplete.Core/AbstractClassStubGenerator.fs +++ b/src/FsAutoComplete.Core/AbstractClassStubGenerator.fs @@ -38,22 +38,13 @@ let private walkTypeDefn (SynTypeDefn (info, repr, members, implicitCtor, range, let otherMembers = allMembers - |> List.filter - - - - - - - - ( - // filter out implicit/explicit constructors and inherit statements, as all members _must_ come after these - function - | SynMemberDefn.ImplicitCtor _ - | SynMemberDefn.ImplicitInherit _ -> false - | SynMemberDefn.Member (SynBinding(valData = SynValData (Some ({ MemberKind = SynMemberKind.Constructor }), _, _)), - _) -> false - | _ -> true) + // filter out implicit/explicit constructors and inherit statements, as all members _must_ come after these + |> List.filter (function + | SynMemberDefn.ImplicitCtor _ + | SynMemberDefn.ImplicitInherit _ -> false + | SynMemberDefn.Member (SynBinding(valData = SynValData (Some ({ MemberKind = SynMemberKind.Constructor }), _, _)), + _) -> false + | _ -> true) match inheritMember with | Some inheritMember -> diff --git a/src/FsAutoComplete.Core/Commands.fs b/src/FsAutoComplete.Core/Commands.fs index 20326fa56..9cd017b42 100644 --- a/src/FsAutoComplete.Core/Commands.fs +++ b/src/FsAutoComplete.Core/Commands.fs @@ -22,6 +22,7 @@ open FSharp.Compiler.Tokenization open SymbolLocation open FSharp.Compiler.Symbols open System.Collections.Immutable +open System.Collections.Generic [] type LocationResponse<'a> = Use of 'a @@ -1058,19 +1059,25 @@ type Commands(checker: FSharpCompilerServiceChecker, state: State, hasAnalyzers: do! findReferencesInFile (file, symbol, p, onFound) }) - let toDict (symbolUseRanges: range seq) = - let dict = new System.Collections.Generic.Dictionary() + let ranges (uses: FSharpSymbolUse[]) = uses |> Array.map (fun u -> u.Range) + + let splitByDeclaration (uses: FSharpSymbolUse[]) = + uses + |> Array.partition (fun u -> u.IsFromDefinition) + + let toDict (symbolUseRanges: range[]) = + let dict = new System.Collections.Generic.Dictionary() symbolUseRanges - |> Seq.collect (fun symbolUse -> + |> Array.collect (fun symbolUse -> let file = symbolUse.FileName // if we had a more complex project system (one that understood that the same file could be in multiple projects distinctly) // then we'd need to map the files to some kind of document identfier and dedupe by that // before issueing the renames. We don't, so this becomes very simple - [ file, symbolUse ]) - |> Seq.groupBy fst - |> Seq.iter (fun (key, items) -> - let itemsSeq = items |> Seq.map snd + [| file, symbolUse |]) + |> Array.groupBy fst + |> Array.iter (fun (key, items) -> + let itemsSeq = items |> Array.map snd dict[key] <- itemsSeq ()) @@ -1090,12 +1097,13 @@ type Commands(checker: FSharpCompilerServiceChecker, state: State, hasAnalyzers: | SymbolDeclarationLocation.CurrentDocument -> let! ct = Async.CancellationToken let symbolUses = tyRes.GetCheckResults.GetUsesOfSymbolInFile(symbol, ct) + let declarations, usages = splitByDeclaration symbolUses + + let declarationRanges, usageRanges = + toDict (ranges declarations), toDict (ranges usages) + + return declarationRanges, usageRanges - return - toDict ( - symbolUses - |> Seq.map (fun symbolUse -> symbolUse.Range) - ) | SymbolDeclarationLocation.Projects (projects, isInternalToProject) -> let symbolUseRanges = ImmutableArray.CreateBuilder() @@ -1120,21 +1128,33 @@ type Commands(checker: FSharpCompilerServiceChecker, state: State, hasAnalyzers: // Distinct these down because each TFM will produce a new 'project'. // Unless guarded by a #if define, symbols with the same range will be added N times let symbolUseRanges = symbolUseRanges.ToArray() |> Array.distinct - return toDict symbolUseRanges + + return Dictionary<_, _> [], toDict symbolUseRanges } member x.RenameSymbol(pos: Position, tyRes: ParseAndCheckResults, lineStr: LineStr, text: NamedText) = asyncResult { - let! symbolUsesByDocument = x.SymbolUseWorkspace(pos, lineStr, text, tyRes) + let! (declarationsByDocument, symbolUsesByDocument) = x.SymbolUseWorkspace(pos, lineStr, text, tyRes) + let totalSetOfRanges = Dictionary() + + for (KeyValue (filePath, declUsages)) in declarationsByDocument do + let! text = state.TryGetFileSource(UMX.tag filePath) + + match totalSetOfRanges.TryGetValue(text) with + | true, ranges -> totalSetOfRanges[text] <- Array.append ranges declUsages + | false, _ -> totalSetOfRanges[text] <- declUsages - let locations = - symbolUsesByDocument - |> Seq.choose (fun (KeyValue (filePath, symbolUses)) -> - match state.TryGetFileSource(UMX.tag filePath) with - | Error _ -> None - | Ok text -> Some(text, symbolUses)) + for (KeyValue (filePath, symbolUses)) in symbolUsesByDocument do + let! text = state.TryGetFileSource(UMX.tag filePath) - return locations + match totalSetOfRanges.TryGetValue(text) with + | true, ranges -> totalSetOfRanges[text] <- Array.append ranges symbolUses + | false, _ -> totalSetOfRanges[text] <- symbolUses + + return + totalSetOfRanges + |> Seq.map (fun (KeyValue (k, v)) -> k, v) + |> Array.ofSeq } member x.SymbolImplementationProject (tyRes: ParseAndCheckResults) (pos: Position) lineStr = diff --git a/src/FsAutoComplete/FsAutoComplete.Lsp.fs b/src/FsAutoComplete/FsAutoComplete.Lsp.fs index 4b86fd0d2..821819c7e 100644 --- a/src/FsAutoComplete/FsAutoComplete.Lsp.fs +++ b/src/FsAutoComplete/FsAutoComplete.Lsp.fs @@ -1383,11 +1383,12 @@ type FSharpLspServer(state: State, lspClient: FSharpLspClient) = p |> x.positionHandler (fun p pos tyRes lineStr lines -> asyncResult { - let! res = + let! declarations, useages = commands.SymbolUseWorkspace(pos, lineStr, lines, tyRes) |> AsyncResult.mapError (JsonRpc.Error.InternalErrorMessage) - let ranges: FSharp.Compiler.Text.Range[] = res.Values |> Seq.concat |> Seq.toArray + let ranges: FSharp.Compiler.Text.Range[] = + useages.Values |> Seq.concat |> Seq.toArray return ranges |> Array.map fcsRangeToLspLocation |> Some }) @@ -1854,11 +1855,12 @@ type FSharpLspServer(state: State, lspClient: FSharpLspClient) = Command = "" Arguments = None } } ) - | Ok uses -> + | Ok (declarations, uses) -> let allUses = uses.Values |> Seq.concat |> Array.ofSeq + // allUses includes the declaration, so we need to reduce it by one to get the number of 'external' references let cmd = - if allUses.Length = 1 then + if allUses.Length = 0 then { Title = "0 References" Command = "" Arguments = None } diff --git a/test/FsAutoComplete.Tests.Lsp/CodeLensTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeLensTests.fs index f412b70b5..e6574d009 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeLensTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeLensTests.fs @@ -6,11 +6,11 @@ open Helpers open Ionide.LanguageServerProtocol.Types open Utils.Server open Utils.ServerTests -open Utils.ServerTests open Utils.TextEdit open Utils.Utils open Utils.CursorbasedTests open Utils.Tests.TextEdit +open Newtonsoft.Json.Linq module private CodeLens = let assertNoDiagnostics (ds: Diagnostic []) = @@ -45,7 +45,7 @@ module private CodeLens = resolved |> List.filter (fun lens -> Range.overlapsStrictly textRange lens.Range) - checkLenses lensesForRange + checkLenses (doc, lensesForRange) } |> AsyncResult.foldResult id (fun e -> failtest $"{e}") @@ -58,7 +58,7 @@ let tests state = """ module X = $0let func x = x + 1$0 - """ (fun lenses -> + """ (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" @@ -67,18 +67,43 @@ let tests state = ) testCaseAsync - "can show codelens for reference count" <| + "can show codelens for 0 reference count" <| CodeLens.check server """ module X = $0let func x = x + 1$0 - """ (fun lenses -> + """ (fun (doc, lenses) -> Expect.hasLength lenses 2 "should have a type lens and a reference lens" let referenceLens = lenses[1] - Expect.equal referenceLens.Command.Value.Title "0 References" "second lens should show the references" - Expect.isSome referenceLens.Command.Value.Arguments "Reference lenses should carry data" - Expect.equal referenceLens.Command.Value.Command "fsharp.showReferences" "Reference lens should call command" + 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 = 1; Character = 8 } "Position should be 0:0" + Expect.hasLength referenceRanges 1 "There should be 1 reference range for the `func` function" + Expect.equal referenceRanges[0] { Uri = doc.Uri; Range = { Start = { Line = 3; Character = 19 }; End = { Line = 3; Character = 23 } } } "Reference range should be 0:0" ) ] )