diff --git a/eng/Versions.props b/eng/Versions.props index 68c623823f..1ceca3d491 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -96,11 +96,11 @@ 6.0.0 4.5.0 - 4.4.0-3.22470.1 - 17.4.196-preview - 17.4.0-preview-3-32916-145 - 17.4.342-pre - 17.4.23-alpha + 4.5.0-1.22520.13 + 17.5.49-preview + 17.5.0-preview-1-33020-520 + 17.5.202-pre-g89e17c9f72 + 17.4.27 17.4.0-preview-22469-04 $(RoslynVersion) @@ -116,7 +116,7 @@ $(MicrosoftVisualStudioShellPackagesVersion) $(MicrosoftVisualStudioShellPackagesVersion) - 17.4.0-preview-3-32916-053 + 17.5.0-preview-1-33019-447 $(MicrosoftVisualStudioShellPackagesVersion) $(MicrosoftVisualStudioShellPackagesVersion) $(MicrosoftVisualStudioShellPackagesVersion) @@ -133,8 +133,8 @@ $(MicrosoftVisualStudioShellPackagesVersion) $(MicrosoftVisualStudioShellPackagesVersion) $(MicrosoftVisualStudioShellPackagesVersion) - 17.4.0-preview-3-32916-053 - 17.4.0-preview-3-32916-053 + 17.5.0-preview-1-33019-447 + 17.5.0-preview-1-33019-447 $(MicrosoftVisualStudioShellPackagesVersion) $(MicrosoftVisualStudioShellPackagesVersion) $(MicrosoftVisualStudioShellPackagesVersion) @@ -171,7 +171,7 @@ 2.3.6152103 17.1.4054 - 17.4.7-alpha + 17.5.9-alpha-g84529e7115 17.0.0 17.0.64 9.0.30729 @@ -204,8 +204,8 @@ 3.11.0 2.1.80 1.0.0-beta2-dev3 - 2.13.23-alpha - 2.9.87-alpha + 2.14.6-alpha + 2.9.112 2.4.1 2.4.2 5.10.3 diff --git a/src/Compiler/Symbols/Symbols.fs b/src/Compiler/Symbols/Symbols.fs index 499654b029..480c58f3af 100644 --- a/src/Compiler/Symbols/Symbols.fs +++ b/src/Compiler/Symbols/Symbols.fs @@ -1777,7 +1777,7 @@ type FSharpMemberOrFunctionOrValue(cenv, d:FSharpMemberOrValData, item) = if isUnresolved() then false else match fsharpInfo() with | None -> false - | Some v -> + | Some v -> v.IsCompilerGenerated member _.InlineAnnotation = diff --git a/vsintegration/src/FSharp.Editor/LanguageService/MetadataAsSource.fs b/vsintegration/src/FSharp.Editor/LanguageService/MetadataAsSource.fs index 6ac4e754c1..0a74deb77c 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/MetadataAsSource.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/MetadataAsSource.fs @@ -94,7 +94,7 @@ module internal MetadataAsSource = [] [); Composition.Shared>] -type internal FSharpMetadataAsSourceService() = +type FSharpMetadataAsSourceService() = let serviceProvider = ServiceProvider.GlobalProvider let projs = System.Collections.Concurrent.ConcurrentDictionary() diff --git a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs index f25746325b..b2fb1792cf 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs @@ -91,7 +91,7 @@ module private CheckerExtensions = } [] -module private ProjectCache = +module internal ProjectCache = /// This is a cache to maintain FSharpParsingOptions and FSharpProjectOptions per Roslyn Project. /// The Roslyn Project is held weakly meaning when it is cleaned up by the GC, the FSharParsingOptions and FSharpProjectOptions will be cleaned up by the GC. @@ -99,9 +99,8 @@ module private ProjectCache = let Projects = ConditionalWeakTable() type Solution with - /// Get the instance of IFSharpWorkspaceService. - member private this.GetFSharpWorkspaceService() = + member internal this.GetFSharpWorkspaceService() = this.Workspace.Services.GetRequiredService() type Document with @@ -247,3 +246,20 @@ type Project with do! doc.FindFSharpReferencesAsync(symbol, (fun textSpan range -> onFound doc textSpan range), userOpName) |> RoslynHelpers.StartAsyncAsTask ct } + + member this.GetFSharpCompilationOptionsAsync(ct: CancellationToken) = + backgroundTask { + if this.IsFSharp then + match ProjectCache.Projects.TryGetValue(this) with + | true, result -> return result + | _ -> + let service = this.Solution.GetFSharpWorkspaceService() + let projectOptionsManager = service.FSharpProjectOptionsManager + match! projectOptionsManager.TryGetOptionsByProject(this, ct) with + | None -> return raise(OperationCanceledException("FSharp project options not found.")) + | Some(parsingOptions, projectOptions) -> + let result = (service.Checker, projectOptionsManager, parsingOptions, projectOptions) + return ProjectCache.Projects.GetValue(this, ConditionalWeakTable<_,_>.CreateValueCallback(fun _ -> result)) + else + return raise(OperationCanceledException("Project is not a FSharp project.")) + } diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs index 4bdfec0bf1..bb84e395b8 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs @@ -16,15 +16,18 @@ open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Navigation open Microsoft.VisualStudio +open Microsoft.VisualStudio.Shell open Microsoft.VisualStudio.Shell.Interop +open Microsoft.VisualStudio.LanguageServices -open FSharp.Compiler open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.EditorServices open FSharp.Compiler.Text open FSharp.Compiler.Text.Range open FSharp.Compiler.Symbols open FSharp.Compiler.Tokenization +open System.Composition +open System.Text.RegularExpressions module private Symbol = @@ -90,7 +93,7 @@ module private ExternalSymbol = |> Option.map (fun args -> upcast methsym, FindDeclExternalSymbol.Constructor(fullTypeName, args)) ) |> List.ofSeq - + (symbol, FindDeclExternalSymbol.Type fullTypeName) :: constructors | :? IMethodSymbol as methsym -> @@ -111,14 +114,14 @@ module private ExternalSymbol = | _ -> [] // TODO: Uncomment code when VS has a fix for updating the status bar. -type internal StatusBar(statusBar: IVsStatusbar) = +type StatusBar(statusBar: IVsStatusbar) = let mutable _searchIcon = int16 Microsoft.VisualStudio.Shell.Interop.Constants.SBAI_Find :> obj let _clear() = // unfreeze the statusbar - statusBar.FreezeOutput 0 |> ignore + statusBar.FreezeOutput 0 |> ignore statusBar.Clear() |> ignore - + member _.Message(_msg: string) = () //let _, frozen = statusBar.IsFrozen() @@ -137,11 +140,11 @@ type internal StatusBar(statusBar: IVsStatusbar) = // | 0, currentText when currentText <> msg -> () // | _ -> clear() //}|> Async.Start - + member _.Clear() = () //clear() /// Animated magnifying glass that displays on the status bar while a symbol search is in progress. - member _.Animate() : IDisposable = + member _.Animate() : IDisposable = //statusBar.Animation (1, &searchIcon) |> ignore { new IDisposable with member _.Dispose() = () } //statusBar.Animation(0, &searchIcon) |> ignore } @@ -155,19 +158,18 @@ type internal FSharpGoToDefinitionResult = | ExternalAssembly of FSharpSymbolUse * MetadataReference seq type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = - - /// Use an origin document to provide the solution & workspace used to + /// Use an origin document to provide the solution & workspace used to /// find the corresponding textSpan and INavigableItem for the range - let rangeToNavigableItem (range: range, document: Document) = + let rangeToNavigableItem (range: range, document: Document) = async { let fileName = try System.IO.Path.GetFullPath range.FileName with _ -> range.FileName let refDocumentIds = document.Project.Solution.GetDocumentIdsWithFilePath fileName - if not refDocumentIds.IsEmpty then + if not refDocumentIds.IsEmpty then let refDocumentId = refDocumentIds.First() let refDocument = document.Project.Solution.GetDocument refDocumentId let! cancellationToken = Async.CancellationToken let! refSourceText = refDocument.GetTextAsync(cancellationToken) |> Async.AwaitTask - match RoslynHelpers.TryFSharpRangeToTextSpan (refSourceText, range) with + match RoslynHelpers.TryFSharpRangeToTextSpan (refSourceText, range) with | None -> return None | Some refTextSpan -> return Some (FSharpGoToDefinitionNavigableItem (refDocument, refTextSpan)) else return None @@ -182,7 +184,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = let! lexerSymbol = originDocument.TryFindFSharpLexerSymbolAsync(position, SymbolLookupKind.Greedy, false, false, userOpName) let textLinePos = sourceText.Lines.GetLinePosition position let fcsTextLineNumber = Line.fromZ textLinePos.Line - let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() + let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() let idRange = lexerSymbol.Ident.idRange let! _, checkFileResults = originDocument.GetFSharpParseAndCheckResultsAsync(nameof(GoToDefinition)) |> liftAsync @@ -191,14 +193,14 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = // if the tooltip was spawned in an implementation file and we have a range targeting // a signature file, try to find the corresponding implementation file and target the // desired symbol - if isSignatureFile fsSymbolUse.FileName && preferSignature = false then + if isSignatureFile fsSymbolUse.FileName && preferSignature = false then let fsfilePath = Path.ChangeExtension (originRange.FileName,"fs") if not (File.Exists fsfilePath) then return! None else let! implDoc = originDocument.Project.Solution.TryGetDocumentFromPath fsfilePath let! implSourceText = implDoc.GetTextAsync () let! _, checkFileResults = implDoc.GetFSharpParseAndCheckResultsAsync(userOpName) |> liftAsync let symbolUses = checkFileResults.GetUsesOfSymbolInFile symbol - let! implSymbol = symbolUses |> Array.tryHead + let! implSymbol = symbolUses |> Array.tryHead let! implTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (implSourceText, implSymbol.Range) return FSharpGoToDefinitionNavigableItem (implDoc, implTextSpan) else @@ -206,10 +208,10 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = return! rangeToNavigableItem (fsSymbolUse.Range, targetDocument) } - /// if the symbol is defined in the given file, return its declaration location, otherwise use the targetSymbol to find the first + /// if the symbol is defined in the given file, return its declaration location, otherwise use the targetSymbol to find the first /// instance of its presence in the provided source file. The first case is needed to return proper declaration location for /// recursive type definitions, where the first its usage may not be the declaration. - member _.FindSymbolDeclarationInDocument(targetSymbolUse: FSharpSymbolUse, document: Document) = + member _.FindSymbolDeclarationInDocument(targetSymbolUse: FSharpSymbolUse, document: Document) = asyncMaybe { let filePath = document.FilePath match targetSymbolUse.Symbol.DeclarationLocation with @@ -217,7 +219,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = | _ -> let! _, checkFileResults = document.GetFSharpParseAndCheckResultsAsync("FindSymbolDeclarationInDocument") |> liftAsync let symbolUses = checkFileResults.GetUsesOfSymbolInFile targetSymbolUse.Symbol - let! implSymbol = symbolUses |> Array.tryHead + let! implSymbol = symbolUses |> Array.tryHead return implSymbol.Range } @@ -229,10 +231,10 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = let textLinePos = sourceText.Lines.GetLinePosition position let textLineString = textLine.ToString() let fcsTextLineNumber = Line.fromZ textLinePos.Line - let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() - + let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() + let preferSignature = isSignatureFile originDocument.FilePath - + let! lexerSymbol = originDocument.TryFindFSharpLexerSymbolAsync(position, SymbolLookupKind.Greedy, false, false, userOpName) let idRange = lexerSymbol.Ident.idRange @@ -262,10 +264,10 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = let! location = symbol.Locations |> Seq.tryHead return (FSharpGoToDefinitionResult.NavigableItem(FSharpGoToDefinitionNavigableItem(project.GetDocument(location.SourceTree), location.SourceSpan)), idRange) | _ -> - let metadataReferences = originDocument.Project.MetadataReferences + let metadataReferences = originDocument.Project.MetadataReferences return (FSharpGoToDefinitionResult.ExternalAssembly(targetSymbolUse, metadataReferences), idRange) - | FindDeclResult.DeclFound targetRange -> + | FindDeclResult.DeclFound targetRange -> // If the file is not associated with a document, it's considered external. if not (originDocument.Project.Solution.ContainsDocumentWithFilePath(targetRange.FileName)) then let metadataReferences = originDocument.Project.MetadataReferences @@ -279,7 +281,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = let implFilePath = Path.ChangeExtension (originDocument.FilePath,"fs") if not (File.Exists implFilePath) then return! None else let! implDocument = originDocument.Project.Solution.TryGetDocumentFromPath implFilePath - + let! targetRange = this.FindSymbolDeclarationInDocument(targetSymbolUse, implDocument) let! implSourceText = implDocument.GetTextAsync(cancellationToken) |> liftTaskAsync let! implTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (implSourceText, targetRange) @@ -288,7 +290,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = else // jump from implementation to the corresponding signature let declarations = checkFileResults.GetDeclarationLocation (fcsTextLineNumber, lexerSymbol.Ident.idRange.EndColumn, textLineString, lexerSymbol.FullIsland, true) match declarations with - | FindDeclResult.DeclFound targetRange -> + | FindDeclResult.DeclFound targetRange -> let! sigDocument = originDocument.Project.Solution.TryGetDocumentFromPath targetRange.FileName let! sigSourceText = sigDocument.GetTextAsync(cancellationToken) |> liftTaskAsync let! sigTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (sigSourceText, targetRange) @@ -296,28 +298,28 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = return (FSharpGoToDefinitionResult.NavigableItem(navItem), idRange) | _ -> return! None - // when the target range is different follow the navigation convention of + // when the target range is different follow the navigation convention of // - gotoDefn origin = signature , gotoDefn destination = signature - // - gotoDefn origin = implementation, gotoDefn destination = implementation + // - gotoDefn origin = implementation, gotoDefn destination = implementation else let! sigDocument = originDocument.Project.Solution.TryGetDocumentFromPath targetRange.FileName let! sigSourceText = sigDocument.GetTextAsync(cancellationToken) |> liftTaskAsync let! sigTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (sigSourceText, targetRange) // if the gotodef call originated from a signature and the returned target is a signature, navigate there - if isSignatureFile targetRange.FileName && preferSignature then + if isSignatureFile targetRange.FileName && preferSignature then let navItem = FSharpGoToDefinitionNavigableItem (sigDocument, sigTextSpan) return (FSharpGoToDefinitionResult.NavigableItem(navItem), idRange) else // we need to get an FSharpSymbol from the targetRange found in the signature // that symbol will be used to find the destination in the corresponding implementation file let implFilePath = // Bugfix: apparently sigDocument not always is a signature file - if isSignatureFile sigDocument.FilePath then Path.ChangeExtension (sigDocument.FilePath, "fs") + if isSignatureFile sigDocument.FilePath then Path.ChangeExtension (sigDocument.FilePath, "fs") else sigDocument.FilePath let! implDocument = originDocument.Project.Solution.TryGetDocumentFromPath implFilePath - - let! targetRange = this.FindSymbolDeclarationInDocument(targetSymbolUse, implDocument) - + + let! targetRange = this.FindSymbolDeclarationInDocument(targetSymbolUse, implDocument) + let! implSourceText = implDocument.GetTextAsync () |> liftTaskAsync let! implTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (implSourceText, targetRange) let navItem = FSharpGoToDefinitionNavigableItem (implDocument, implTextSpan) @@ -326,7 +328,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = return! None } - /// find the declaration location (signature file/.fsi) of the target symbol if possible, fall back to definition + /// find the declaration location (signature file/.fsi) of the target symbol if possible, fall back to definition member this.FindDeclarationOfSymbolAtRange(targetDocument: Document, symbolRange: range, targetSource: SourceText) = this.FindSymbolHelper(targetDocument, symbolRange, targetSource, preferSignature=true) @@ -341,7 +343,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = >> Array.toSeq) |> RoslynHelpers.StartAsyncAsTask cancellationToken - /// Construct a task that will return a navigation target for the implementation definition of the symbol + /// Construct a task that will return a navigation target for the implementation definition of the symbol /// at the provided position in the document. member this.FindDefinitionTask(originDocument: Document, position: int, cancellationToken: CancellationToken) = this.FindDefinitionAtPosition(originDocument, position, cancellationToken) @@ -355,7 +357,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = let navigationService = workspace.Services.GetService() let navigationSucceeded = navigationService.TryNavigateToSpan(workspace, navigableItem.Document.Id, navigableItem.SourceSpan, cancellationToken) - if not navigationSucceeded then + if not navigationSucceeded then statusBar.TempMessage (SR.CannotNavigateUnknown()) member _.NavigateToItem(navigableItem: FSharpNavigableItem, statusBar: StatusBar, cancellationToken: CancellationToken) = @@ -368,13 +370,13 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = // Prefer open documents in the preview tab. let result = navigationService.TryNavigateToSpan(workspace, navigableItem.Document.Id, navigableItem.SourceSpan, cancellationToken) - - if result then + + if result then statusBar.Clear() - else + else statusBar.TempMessage (SR.CannotNavigateUnknown()) - /// Find the declaration location (signature file/.fsi) of the target symbol if possible, fall back to definition + /// Find the declaration location (signature file/.fsi) of the target symbol if possible, fall back to definition member this.NavigateToSymbolDeclarationAsync(targetDocument: Document, targetSourceText: SourceText, symbolRange: range, statusBar: StatusBar, cancellationToken: CancellationToken) = asyncMaybe { let! item = this.FindDeclarationOfSymbolAtRange(targetDocument, symbolRange, targetSourceText) @@ -406,10 +408,10 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = let result = match textOpt with | Some (text, fileName) -> - let tmpProjInfo, tmpDocInfo = + let tmpProjInfo, tmpDocInfo = MetadataAsSource.generateTemporaryDocument( - AssemblyIdentity(targetSymbolUse.Symbol.Assembly.QualifiedName), - fileName, + AssemblyIdentity(targetSymbolUse.Symbol.Assembly.QualifiedName), + fileName, metadataReferences) let tmpShownDocOpt = metadataAsSource.ShowDocument(tmpProjInfo, tmpDocInfo.FilePath, SourceText.From(text.ToString())) match tmpShownDocOpt with @@ -427,7 +429,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = ty1.GenericArguments.Count = ty2.GenericArguments.Count && (ty1.GenericArguments, ty2.GenericArguments) ||> Seq.forall2 areTypesEqual - ) + ) if generic then true else @@ -436,7 +438,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = namesEqual && accessPathsEqual // This tries to find the best possible location of the target symbol's location in the metadata source. - // We really should rely on symbol equality within FCS instead of doing it here, + // We really should rely on symbol equality within FCS instead of doing it here, // but the generated metadata as source isn't perfect for symbol equality. checkResults.GetAllUsesOfAllSymbolsInFile(cancellationToken) |> Seq.tryFind (fun x -> @@ -470,7 +472,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = | Some span -> span | _ -> TextSpan() - return span + return span } let span = @@ -478,7 +480,7 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = | Some span -> span | _ -> TextSpan() - let navItem = FSharpGoToDefinitionNavigableItem(tmpShownDoc, span) + let navItem = FSharpGoToDefinitionNavigableItem(tmpShownDoc, span) this.NavigateToItem(navItem, statusBar, cancellationToken) true | _ -> @@ -486,9 +488,9 @@ type internal GoToDefinition(metadataAsSource: FSharpMetadataAsSourceService) = | _ -> false - if result then + if result then statusBar.Clear() - else + else statusBar.TempMessage (SR.CannotNavigateUnknown()) type internal QuickInfo = @@ -526,7 +528,7 @@ module internal FSharpQuickInfo = let! extLexerSymbol = extDocument.TryFindFSharpLexerSymbolAsync(extSpan.Start, SymbolLookupKind.Greedy, true, true, userOpName) let! _, extCheckFileResults = extDocument.GetFSharpParseAndCheckResultsAsync(userOpName) |> liftAsync - let extQuickInfoText = + let extQuickInfoText = extCheckFileResults.GetToolTip (declRange.StartLine, extLexerSymbol.Ident.idRange.EndColumn, extLineText, extLexerSymbol.FullIsland, FSharpTokenTag.IDENT) @@ -558,7 +560,7 @@ module internal FSharpQuickInfo = let! lexerSymbol = document.TryFindFSharpLexerSymbolAsync(position, SymbolLookupKind.Greedy, true, true, userOpName) let! _, checkFileResults = document.GetFSharpParseAndCheckResultsAsync(userOpName) |> liftAsync let! sourceText = document.GetTextAsync cancellationToken - let idRange = lexerSymbol.Ident.idRange + let idRange = lexerSymbol.Ident.idRange let textLinePos = sourceText.Lines.GetLinePosition position let fcsTextLineNumber = Line.fromZ textLinePos.Line let lineText = (sourceText.Lines.GetLineFromPosition position).ToString() @@ -584,13 +586,13 @@ module internal FSharpQuickInfo = SymbolKind = lexerSymbol.Kind } } - match lexerSymbol.Kind with + match lexerSymbol.Kind with | LexerSymbolKind.Keyword | LexerSymbolKind.String -> let! targetQuickInfo = getTargetSymbolQuickInfo (None, FSharpTokenTag.STRING) return lexerSymbol.Range, None, Some targetQuickInfo - - | _ -> + + | _ -> let! symbolUse = checkFileResults.GetSymbolUseAtLocation (fcsTextLineNumber, idRange.EndColumn, lineText, lexerSymbol.FullIsland) // if the target is in a signature file, adjusting the quick info is unnecessary @@ -606,7 +608,7 @@ module internal FSharpQuickInfo = let! targetQuickInfo = getTargetSymbolQuickInfo (Some symbolUse.Symbol, FSharpTokenTag.IDENT) let! result = - match findSigDeclarationResult with + match findSigDeclarationResult with | FindDeclResult.DeclFound declRange when isSignatureFile declRange.FileName -> asyncMaybe { let! sigQuickInfo = getQuickInfoFromRange(document, declRange, cancellationToken) @@ -666,11 +668,11 @@ type internal FSharpNavigation // To ensure proper navigation decsions, we need to check the type of document the navigation call // is originating from and the target we're provided by default: - // - signature files (.fsi) should navigate to other signature files + // - signature files (.fsi) should navigate to other signature files // - implementation files (.fs) should navigate to other implementation files let (|Signature|Implementation|) filepath = if isSignatureFile filepath then Signature else Implementation - + match initialDoc.FilePath, targetPath with | Signature, Signature | Implementation, Implementation -> @@ -679,7 +681,7 @@ type internal FSharpNavigation // Adjust the target from signature to implementation. | Implementation, Signature -> return! gtd.NavigateToSymbolDefinitionAsync(targetDoc, targetSource, range, statusBar, cancellationToken) - + // Adjust the target from implmentation to signature. | Signature, Implementation -> return! gtd.NavigateToSymbolDeclarationAsync(targetDoc, targetSource, range, statusBar, cancellationToken) @@ -699,10 +701,10 @@ type internal FSharpNavigation ) |> Task.FromResult - member this.TryGoToDefinition(position, cancellationToken) = + member _.TryGoToDefinition(position, cancellationToken) = let gtd = GoToDefinition(metadataAsSource) let gtdTask = gtd.FindDefinitionTask(initialDoc, position, cancellationToken) - + // Wrap this in a try/with as if the user clicks "Cancel" on the thread dialog, we'll be cancelled. // Task.Wait throws an exception if the task is cancelled, so be sure to catch it. try @@ -718,12 +720,244 @@ type internal FSharpNavigation gtd.NavigateToExternalDeclaration(targetSymbolUse, metadataReferences, cancellationToken, statusBar) // 'true' means do it, like Sheev Palpatine would want us to. true - else + else statusBar.TempMessage (SR.CannotDetermineSymbol()) false - with exc -> + with exc -> statusBar.TempMessage(String.Format(SR.NavigateToFailed(), Exception.flattenMessage exc)) - + // Don't show the dialog box as it's most likely that the user cancelled. // Don't make them click twice. - true \ No newline at end of file + true + +[] +type internal SymbolMemberType = + Event | Property | Method | Constructor | Other + static member FromString(s: string) = + match s with + | "E" -> Event + | "P" -> Property + | "CTOR" -> Constructor // That one is "artificial one", so we distinguish constructors. + | "M" -> Method + | _ -> Other + +type internal SymbolPath = { EntityPath: string list; MemberOrValName: string; GenericParameters: int } + +[] +type internal DocCommentId = + | Member of SymbolPath * SymbolMemberType: SymbolMemberType + | Field of SymbolPath + | Type of EntityPath: string list + | None + +type FSharpNavigableLocation(statusBar: StatusBar, metadataAsSource: FSharpMetadataAsSourceService, symbolRange: range, project: Project) = + interface IFSharpNavigableLocation with + member _.NavigateToAsync(_options: FSharpNavigationOptions2, cancellationToken: CancellationToken) : Task = + asyncMaybe { + let targetPath = symbolRange.FileName + let! targetDoc = project.Solution.TryGetDocumentFromFSharpRange (symbolRange, project.Id) + let! targetSource = targetDoc.GetTextAsync(cancellationToken) + let gtd = GoToDefinition(metadataAsSource) + + let (|Signature|Implementation|) filepath = + if isSignatureFile filepath then Signature else Implementation + + match targetPath with + | Signature -> + return! gtd.NavigateToSymbolDefinitionAsync(targetDoc, targetSource, symbolRange, statusBar, cancellationToken) + | Implementation -> + return! gtd.NavigateToSymbolDeclarationAsync(targetDoc, targetSource, symbolRange, statusBar, cancellationToken) + } + |> Async.map (fun a -> a.IsSome) + |> RoslynHelpers.StartAsyncAsTask cancellationToken + +[)>] +[)>] +type FSharpCrossLanguageSymbolNavigationService() = + let componentModel = Package.GetGlobalService(typeof) :?> ComponentModelHost.IComponentModel + let workspace = componentModel.GetService() + let statusBar = StatusBar(ServiceProvider.GlobalProvider.GetService()) + let metadataAsSource = componentModel.DefaultExportProvider.GetExport().Value + + let tryFindFieldByName (name: string) (e: FSharpEntity) = + let fields = + e.FSharpFields + |> Seq.filter ( + fun x -> + x.DisplayName = name + && not x.IsCompilerGenerated) + |> Seq.map (fun e -> e.DeclarationLocation) + + if fields.Count() <= 0 && (e.IsFSharpUnion || e.IsFSharpRecord) then + Seq.singleton e.DeclarationLocation + else + fields + + let tryFindValByNameAndType (name: string) (symbolMemberType: SymbolMemberType) (genericParametersCount: int) (e: FSharpEntity) (entities: FSharpMemberOrFunctionOrValue seq) = + + let defaultFilter (e: FSharpMemberOrFunctionOrValue) = + (e.DisplayName = name || e.CompiledName = name) && + e.GenericParameters.Count = genericParametersCount + + let isProperty (e: FSharpMemberOrFunctionOrValue) = defaultFilter e && e.IsProperty + let isConstructor (e: FSharpMemberOrFunctionOrValue) = defaultFilter e && e.IsConstructor + + let getLocation (e: FSharpMemberOrFunctionOrValue) = e.DeclarationLocation + + let filteredEntities: range seq = + match symbolMemberType with + | SymbolMemberType.Other + | SymbolMemberType.Method -> + entities + |> Seq.filter defaultFilter + |> Seq.map getLocation + // F# record-specific logic, if navigating to the record's ctor, then navigate to record declaration. + // If we navigating to F# record property, we first check if it's "custom" property, if it's one of the record fields, we search for it in the fields. + | SymbolMemberType.Constructor when e.IsFSharpRecord -> + Seq.singleton e.DeclarationLocation + | SymbolMemberType.Property when e.IsFSharpRecord -> + let properties = + entities + |> Seq.filter isProperty + |> Seq.map getLocation + let fields = tryFindFieldByName name e + Seq.append properties fields + | SymbolMemberType.Constructor -> + entities + |> Seq.filter isConstructor + |> Seq.map getLocation + // When navigating to property for the record, it will be in members bag for custom ones, but will be in the fields in fields. + | SymbolMemberType.Event // Events are just properties` + | SymbolMemberType.Property -> + entities + |> Seq.filter isProperty + |> Seq.map getLocation + + filteredEntities + + let tryFindVal (name: string) (documentCommentId: string) (symbolMemberType: SymbolMemberType) (genericParametersCount: int) (e: FSharpEntity) = + let entities = e.TryGetMembersFunctionsAndValues() + + // First, try and find entity by exact xml signature, return if found, + // otherwise, just try and match by parsed name and number of arguments. + + let entitiesByXmlSig = + entities + |> Seq.filter (fun e -> e.XmlDocSig = documentCommentId) + |> Seq.map (fun e -> e.DeclarationLocation) + + if Seq.isEmpty entitiesByXmlSig then + tryFindValByNameAndType name symbolMemberType genericParametersCount e entities + else + entitiesByXmlSig + + static member internal DocCommentIdToPath (docId:string) = + // The groups are following: + // 1 - type (see below). + // 2 - Path - a dotted path to a symbol. + // 3 - parameters, opetional, only for methods and properties. + // 4 - return type, optional, only for methods. + let docCommentIdRx = Regex(@"^(?\w):(?[\w\d#`.]+)(?\(.+\))?(?:~([\w\d.]+))?$", RegexOptions.Compiled) + + // Parse generic args out of the function name + let fnGenericArgsRx = Regex(@"^(?.+)``(?\d+)$", RegexOptions.Compiled) + // docCommentId is in the following format: + // + // "T:" prefix for types + // "T:N.X.Nested" - type + // "T:N.X.D" - delegate + // + // "M:" prefix is for methods + // "M:N.X.#ctor" - constructor + // "M:N.X.#ctor(System.Int32)" - constructor with one parameter + // "M:N.X.f" - method with unit parameter + // "M:N.X.bb(System.String,System.Int32@)" - method with two parameters + // "M:N.X.gg(System.Int16[],System.Int32[0:,0:])" - method with two parameters, 1d and 2d array + // "M:N.X.op_Addition(N.X,N.X)" - operator + // "M:N.X.op_Explicit(N.X)~System.Int32" - operator with return type + // "M:N.GenericMethod.WithNestedType``1(N.GenericType{``0}.NestedType)" - generic type with one parameter + // "M:N.GenericMethod.WithIntOfNestedType``1(N.GenericType{System.Int32}.NestedType)" - generic type with one parameter + // "M:N.X.N#IX{N#KVP{System#String,System#Int32}}#IXA(N.KVP{System.String,System.Int32})" - explicit interface implementation + // + // "E:" prefix for events + // + // "E:N.X.d". + // + // "F:" prefix for fields + // "F:N.X.q" - field + // + // "P:" prefix for properties + // "P:N.X.prop" - property with getter and setter + + let m = docCommentIdRx.Match(docId) + let t = m.Groups["kind"].Value + match m.Success, t with + | true, ("M" | "P" | "E") -> + // TODO: Probably, there's less janky way of dealing with those. + let parts = m.Groups["entity"].Value.Split('.') + let entityPath = parts[..(parts.Length - 2)] |> List.ofArray + let memberOrVal = parts[parts.Length - 1] + + // Try and parse generic params count from the name (e.g. NameOfTheFunction``1, where ``1 is amount of type parameters) + let genericM = fnGenericArgsRx.Match(memberOrVal) + let (memberOrVal, genericParametersCount) = + if genericM.Success then + (genericM.Groups["entity"].Value, int genericM.Groups["typars"].Value) + else + memberOrVal, 0 + + // A hack/fixup for the constructor name (#ctor in doccommentid and ``.ctor`` in F#) + if memberOrVal = "#ctor" then + DocCommentId.Member ({ EntityPath = entityPath; MemberOrValName = "``.ctor``"; GenericParameters = 0 },SymbolMemberType.Constructor) + else + DocCommentId.Member ({ EntityPath = entityPath; MemberOrValName = memberOrVal; GenericParameters = genericParametersCount }, (SymbolMemberType.FromString t)) + | true, "T" -> + let entityPath = m.Groups["entity"].Value.Split('.') |> List.ofArray + DocCommentId.Type entityPath + | true, "F" -> + let parts = m.Groups["entity"].Value.Split('.') + let entityPath = parts[..(parts.Length - 2)] |> List.ofArray + let memberOrVal = parts[parts.Length - 1] + DocCommentId.Field { EntityPath = entityPath; MemberOrValName = memberOrVal; GenericParameters = 0 } + | _ -> DocCommentId.None + + interface IFSharpCrossLanguageSymbolNavigationService with + member _.TryGetNavigableLocationAsync(assemblyName: string, documentationCommentId: string, cancellationToken: CancellationToken) : Task = + let path = FSharpCrossLanguageSymbolNavigationService.DocCommentIdToPath documentationCommentId + backgroundTask { + let projects = workspace.CurrentSolution.Projects |> Seq.filter (fun p -> p.IsFSharp && p.AssemblyName = assemblyName) + + let mutable locations = Seq.empty + + for project in projects do + let! checker, _, _, options = project.GetFSharpCompilationOptionsAsync(cancellationToken) + let! result = checker.ParseAndCheckProject(options) + + match path with + | DocCommentId.Member ({ EntityPath = entityPath; MemberOrValName = memberOrVal; GenericParameters = genericParametersCount }, memberType) -> + let entity = result.AssemblySignature.FindEntityByPath (entityPath) + entity |> Option.iter (fun e -> + locations <- e |> tryFindVal memberOrVal documentationCommentId memberType genericParametersCount + |> Seq.map (fun m -> (m, project)) + |> Seq.append locations) + | DocCommentId.Field { EntityPath = entityPath; MemberOrValName = memberOrVal } -> + let entity = result.AssemblySignature.FindEntityByPath (entityPath) + entity |> Option.iter (fun e -> + locations <- e |> tryFindFieldByName memberOrVal + |> Seq.map (fun m -> (m, project)) + |> Seq.append locations) + | DocCommentId.Type entityPath -> + let entity = result.AssemblySignature.FindEntityByPath (entityPath) + entity |> Option.iter (fun e -> + locations <- Seq.append locations [e.DeclarationLocation, project]) + | DocCommentId.None -> () + + // TODO: Figure out the way of giving the user choice where to navigate, if there are more than one result + // For now, we only take 1st one, since it's usually going to be only one result (given we process names correctly). + // More results can theoretically be returned in case of method overloads, or when we have both signature and implementation files. + if locations.Count() >= 1 then + let (location, project) = locations.First() + return FSharpNavigableLocation(statusBar, metadataAsSource, location, project) :> IFSharpNavigableLocation + else + return Unchecked.defaultof<_> // returning null here, so Roslyn can fallback to default source-as-metadata implementation. + } diff --git a/vsintegration/tests/UnitTests/DocCommentIdParserTests.fs b/vsintegration/tests/UnitTests/DocCommentIdParserTests.fs new file mode 100644 index 0000000000..b9cc39f66d --- /dev/null +++ b/vsintegration/tests/UnitTests/DocCommentIdParserTests.fs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. +[] +module Tests.ServiceAnalysis.DocCommentIdParser + +open NUnit.Framework +open Microsoft.VisualStudio.FSharp.Editor + + + + +[] +let ``Test DocCommentId parser``() = + let testData = dict [ + "T:N.X.Nested", DocCommentId.Type ["N"; "X"; "Nested"]; + "M:N.X.#ctor", DocCommentId.Member ({ EntityPath = ["N"; "X"]; MemberOrValName = "``.ctor``"; GenericParameters = 0 }, SymbolMemberType.Constructor); + "M:N.X.#ctor(System.Int32)", DocCommentId.Member ({ EntityPath = ["N"; "X"]; MemberOrValName = "``.ctor``"; GenericParameters = 0 }, SymbolMemberType.Constructor); + "M:N.X.f", DocCommentId.Member ({ EntityPath = ["N"; "X"]; MemberOrValName = "f"; GenericParameters = 0 }, SymbolMemberType.Method); + "M:N.X.bb(System.String,System.Int32@)", DocCommentId.Member ({ EntityPath = ["N"; "X"]; MemberOrValName = "bb"; GenericParameters = 0 }, SymbolMemberType.Method); + "M:N.X.gg(System.Int16[],System.Int32[0:,0:])", DocCommentId.Member ({ EntityPath = ["N"; "X"]; MemberOrValName = "gg"; GenericParameters = 0 }, SymbolMemberType.Method); + "M:N.X.op_Addition(N.X,N.X)", DocCommentId.Member ({ EntityPath = ["N"; "X"]; MemberOrValName = "op_Addition"; GenericParameters = 0 }, SymbolMemberType.Method); + "M:N.X.op_Explicit(N.X)~System.Int32", DocCommentId.Member ({ EntityPath = ["N"; "X"]; MemberOrValName = "op_Explicit"; GenericParameters = 0 }, SymbolMemberType.Method); + "M:N.GenericMethod.WithNestedType``1(N.GenericType{``0}.NestedType)", DocCommentId.Member ({ EntityPath = ["N"; "GenericMethod"]; MemberOrValName = "WithNestedType"; GenericParameters = 1 }, SymbolMemberType.Method); + "M:N.GenericMethod.WithIntOfNestedType``1(N.GenericType{System.Int32}.NestedType)", DocCommentId.Member ({ EntityPath = ["N"; "GenericMethod"]; MemberOrValName = "WithIntOfNestedType"; GenericParameters = 1 }, SymbolMemberType.Method); + "E:N.X.d", DocCommentId.Member ({ EntityPath = ["N"; "X"]; MemberOrValName = "d"; GenericParameters = 0 }, SymbolMemberType.Event); + "F:N.X.q", DocCommentId.Field { EntityPath = ["N"; "X"]; MemberOrValName = "q"; GenericParameters = 0 }; + "P:N.X.prop", DocCommentId.Member ({ EntityPath = ["N"; "X"]; MemberOrValName = "prop"; GenericParameters = 0 }, SymbolMemberType.Property); + ] + + let mutable res = "" + for pair in testData do + let docId = pair.Key + let expected = pair.Value + let actual = FSharpCrossLanguageSymbolNavigationService.DocCommentIdToPath(docId) + if actual <> expected then + res <- res + $"DocumentId: {docId}; Expected = %A{expected} = Actual = %A{actual}\n" + + if res <> "" then + failwith res + () \ No newline at end of file diff --git a/vsintegration/tests/UnitTests/VisualFSharp.UnitTests.fsproj b/vsintegration/tests/UnitTests/VisualFSharp.UnitTests.fsproj index 8bfcdc25ff..1ee9d2b4a4 100644 --- a/vsintegration/tests/UnitTests/VisualFSharp.UnitTests.fsproj +++ b/vsintegration/tests/UnitTests/VisualFSharp.UnitTests.fsproj @@ -57,6 +57,9 @@ + + CompilerService\DocCommentIdParserTests.fs + CompilerService\UnusedOpensTests.fs