Skip to content
Merged
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
1 change: 1 addition & 0 deletions vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
<Compile Include="LanguageService\FSharpCheckerExtensions.fs" />
<Compile Include="LanguageService\IProjectSite.fs" />
<Compile Include="LanguageService\ProvideFSharpVersionRegistrationAttribute.fs" />
<Compile Include="LanguageService\MetadataAsSource.fs" />
<Compile Include="LanguageService\FSharpCheckerProvider.fs" />
<Compile Include="LanguageService\FSharpProjectOptionsManager.fs" />
<Compile Include="LanguageService\SingleFileWorkspaceMap.fs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type internal FSharpCheckerProvider
settings: EditorOptions
) =

let metadataAsSource = FSharpMetadataAsSourceService()

let tryGetMetadataSnapshot (path, timeStamp) =
try
let md = Microsoft.CodeAnalysis.ExternalAccess.FSharp.LanguageServices.FSharpVisualStudioWorkspaceExtensions.GetMetadata(workspace, path, timeStamp)
Expand Down Expand Up @@ -85,3 +87,5 @@ type internal FSharpCheckerProvider

member this.Checker = checker.Value

member _.MetadataAsSource = metadataAsSource

Original file line number Diff line number Diff line change
Expand Up @@ -433,3 +433,5 @@ type internal FSharpProjectOptionsManager
reactor.SetCpsCommandLineOptions(projectId, sourcePaths, options.ToArray())

member _.Checker = checkerProvider.Checker

member _.MetadataAsSource = checkerProvider.MetadataAsSource
156 changes: 156 additions & 0 deletions vsintegration/src/FSharp.Editor/LanguageService/MetadataAsSource.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

namespace Microsoft.VisualStudio.FSharp.Editor

open System
open System.Threading
open System.Collections.Immutable
open System.Diagnostics
open System.IO
open System.Linq
open System.Text
open System.Runtime.InteropServices
open System.Reflection.PortableExecutable

open Microsoft.CodeAnalysis
open Microsoft.CodeAnalysis.FindSymbols
open Microsoft.CodeAnalysis.Text
open Microsoft.CodeAnalysis.Navigation
open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Navigation
open Microsoft.VisualStudio.ComponentModelHost

open Microsoft.VisualStudio
open Microsoft.VisualStudio.Editor
open Microsoft.VisualStudio.Threading
open Microsoft.VisualStudio.Shell
open Microsoft.VisualStudio.Shell.Interop
open Microsoft.VisualStudio.TextManager.Interop

open FSharp.Compiler.SourceCodeServices
open FSharp.Compiler.Text

module internal MetadataAsSource =

open Microsoft.CodeAnalysis.CSharp
open ICSharpCode.Decompiler
open ICSharpCode.Decompiler.CSharp
open ICSharpCode.Decompiler.Metadata
open ICSharpCode.Decompiler.CSharp.Transforms
open ICSharpCode.Decompiler.TypeSystem

let generateTemporaryCSharpDocument (asmIdentity: AssemblyIdentity, name: string, metadataReferences) =
let rootPath = Path.Combine(Path.GetTempPath(), "MetadataAsSource")
let extension = ".cs"
let directoryName = Guid.NewGuid().ToString("N")
let temporaryFilePath = Path.Combine(rootPath, directoryName, name + extension)

let projectId = ProjectId.CreateNewId()

let parseOptions = CSharpParseOptions.Default.WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.Preview)
// Just say it's always a DLL since we probably won't have a Main method
let compilationOptions = Microsoft.CodeAnalysis.CSharp.CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)

// We need to include the version information of the assembly so InternalsVisibleTo and stuff works
let assemblyInfoDocumentId = DocumentId.CreateNewId(projectId)
let assemblyInfoFileName = "AssemblyInfo" + extension
let assemblyInfoString = String.Format(@"[assembly: System.Reflection.AssemblyVersion(""{0}"")]", asmIdentity.Version)

let assemblyInfoSourceTextContainer = SourceText.From(assemblyInfoString, Encoding.UTF8).Container

let assemblyInfoDocument =
DocumentInfo.Create(
assemblyInfoDocumentId,
assemblyInfoFileName,
loader = TextLoader.From(assemblyInfoSourceTextContainer, VersionStamp.Default))

let generatedDocumentId = DocumentId.CreateNewId(projectId)
let documentInfo =
DocumentInfo.Create(
generatedDocumentId,
Path.GetFileName(temporaryFilePath),
filePath = temporaryFilePath,
loader = FileTextLoader(temporaryFilePath, Encoding.UTF8))

let projectInfo =
ProjectInfo.Create(
projectId,
VersionStamp.Default,
name = asmIdentity.Name,
assemblyName = asmIdentity.Name,
language = LanguageNames.CSharp,
compilationOptions = compilationOptions,
parseOptions = parseOptions,
documents = [|assemblyInfoDocument;documentInfo|],
metadataReferences = metadataReferences)

(projectInfo, documentInfo)

let decompileCSharp (symbolFullTypeName: string, assemblyLocation: string) =
let logger = new StringBuilder()

// Initialize a decompiler with default settings.
let decompiler = CSharpDecompiler(assemblyLocation, DecompilerSettings())
// Escape invalid identifiers to prevent Roslyn from failing to parse the generated code.
// (This happens for example, when there is compiler-generated code that is not yet recognized/transformed by the decompiler.)
decompiler.AstTransforms.Add(new EscapeInvalidIdentifiers())

let fullTypeName = FullTypeName(symbolFullTypeName)

// Try to decompile; if an exception is thrown the caller will handle it
let text = decompiler.DecompileTypeAsString(fullTypeName)

let text = text + "#if false // " + Environment.NewLine
let text = text + logger.ToString()
let text = text + "#endif" + Environment.NewLine

SourceText.From(text)

let showDocument (filePath, name, serviceProvider: IServiceProvider) =
let vsRunningDocumentTable4 = serviceProvider.GetService<SVsRunningDocumentTable, IVsRunningDocumentTable4>()
let fileAlreadyOpen = vsRunningDocumentTable4.IsMonikerValid(filePath)

let openDocumentService = serviceProvider.GetService<SVsUIShellOpenDocument, IVsUIShellOpenDocument>()

let (_, _, _, _, windowFrame) = openDocumentService.OpenDocumentViaProject(filePath, ref VSConstants.LOGVIEWID.TextView_guid)

let componentModel = serviceProvider.GetService<SComponentModel, IComponentModel>()
let editorAdaptersFactory = componentModel.GetService<IVsEditorAdaptersFactoryService>();
let documentCookie = vsRunningDocumentTable4.GetDocumentCookie(filePath)
let vsTextBuffer = vsRunningDocumentTable4.GetDocumentData(documentCookie) :?> IVsTextBuffer
let textBuffer = editorAdaptersFactory.GetDataBuffer(vsTextBuffer)

if not fileAlreadyOpen then
ErrorHandler.ThrowOnFailure(windowFrame.SetProperty(int __VSFPROPID5.VSFPROPID_IsProvisional, true)) |> ignore
ErrorHandler.ThrowOnFailure(windowFrame.SetProperty(int __VSFPROPID5.VSFPROPID_OverrideCaption, name)) |> ignore
ErrorHandler.ThrowOnFailure(windowFrame.SetProperty(int __VSFPROPID5.VSFPROPID_OverrideToolTip, name)) |> ignore

windowFrame.Show() |> ignore

let textContainer = textBuffer.AsTextContainer()
let mutable workspace = Unchecked.defaultof<_>
if Workspace.TryGetWorkspace(textContainer, &workspace) then
let solution = workspace.CurrentSolution
let documentId = workspace.GetDocumentIdInCurrentContext(textContainer)
match box documentId with
| null -> None
| _ -> solution.GetDocument(documentId) |> Some
else
None

[<Sealed>]
type internal FSharpMetadataAsSourceService() =

member val CSharpFiles = System.Collections.Concurrent.ConcurrentDictionary(StringComparer.OrdinalIgnoreCase)

member this.ShowCSharpDocument(projInfo: ProjectInfo, docInfo: DocumentInfo, text: Text.SourceText) =
let _ =
let directoryName = Path.GetDirectoryName(docInfo.FilePath)
if Directory.Exists(directoryName) |> not then
Directory.CreateDirectory(directoryName) |> ignore
use fileStream = new FileStream(docInfo.FilePath, IO.FileMode.Create)
use writer = new StreamWriter(fileStream)
text.Write(writer)

this.CSharpFiles.[docInfo.FilePath] <- (projInfo, docInfo)

MetadataAsSource.showDocument(docInfo.FilePath, docInfo.Name, ServiceProvider.GlobalProvider)
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,33 @@ type internal SingleFileWorkspaceMap(workspace: VisualStudioWorkspace,
projectContext.AddSourceFile(filePath, sourceCodeKind = createSourceCodeKind filePath)
projectContext

let createCSharpMetadataProjectContext (projInfo: ProjectInfo) (docInfo: DocumentInfo) =
let projectContext = projectContextFactory.CreateProjectContext(LanguageNames.CSharp, projInfo.Id.ToString(), projInfo.FilePath, Guid.NewGuid(), null, null)
projectContext.DisplayName <- projInfo.Name
projectContext.AddSourceFile(docInfo.FilePath, sourceCodeKind = SourceCodeKind.Regular)

for metaRef in projInfo.MetadataReferences do
match metaRef with
| :? PortableExecutableReference as peRef ->
projectContext.AddMetadataReference(peRef.FilePath, MetadataReferenceProperties.Assembly)
| _ ->
()

projectContext

do
miscFilesWorkspace.DocumentOpened.Add(fun args ->
let document = args.Document

if document.Project.Language = FSharpConstants.FSharpLanguageName && workspace.CurrentSolution.GetDocumentIdsWithFilePath(document.FilePath).Length = 0 then
files.[document.FilePath] <- createProjectContext document.FilePath

if optionsManager.MetadataAsSource.CSharpFiles.ContainsKey(document.FilePath) && workspace.CurrentSolution.GetDocumentIdsWithFilePath(document.FilePath).Length = 0 then
match optionsManager.MetadataAsSource.CSharpFiles.TryGetValue(document.FilePath) with
| true, (projInfo, docInfo) ->
files.[document.FilePath] <- createCSharpMetadataProjectContext projInfo docInfo
| _ ->
()
)

workspace.DocumentOpened.Add(fun args ->
Expand All @@ -57,6 +79,8 @@ type internal SingleFileWorkspaceMap(workspace: VisualStudioWorkspace,
optionsManager.ClearSingleFileOptionsCache(document.Id)
projectContext.Dispose()
| _ -> ()

optionsManager.MetadataAsSource.CSharpFiles.TryRemove(document.FilePath) |> ignore
)

do
Expand Down
66 changes: 42 additions & 24 deletions vsintegration/src/FSharp.Editor/Navigation/GoToDefinition.fs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,28 @@ open System.Collections.Immutable
open System.Diagnostics
open System.IO
open System.Linq
open System.Text
open System.Runtime.InteropServices
open System.Reflection.PortableExecutable

open Microsoft.CodeAnalysis
open Microsoft.CodeAnalysis.FindSymbols
open Microsoft.CodeAnalysis.Text
open Microsoft.CodeAnalysis.Navigation
open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Navigation
open Microsoft.VisualStudio.ComponentModelHost

open Microsoft.VisualStudio
open Microsoft.VisualStudio.Editor
open Microsoft.VisualStudio.Threading
open Microsoft.VisualStudio.Shell
open Microsoft.VisualStudio.Shell.Interop
open Microsoft.VisualStudio.TextManager.Interop

open FSharp.Compiler.SourceCodeServices
open FSharp.Compiler.Text


module private Symbol =
let fullName (root: ISymbol) : string =
let rec inner parts (sym: ISymbol) =
Expand Down Expand Up @@ -145,6 +154,11 @@ type internal StatusBar(statusBar: IVsStatusbar) =
type internal FSharpGoToDefinitionNavigableItem(document, sourceSpan) =
inherit FSharpNavigableItem(Glyph.BasicFile, ImmutableArray.Empty, document, sourceSpan)

[<RequireQualifiedAccess>]
type internal FSharpGoToDefinitionResult =
| NavigableItem of FSharpNavigableItem
| ExternalAssembly of ProjectInfo * DocumentInfo * FSharpSymbolUse * FSharpExternalSymbol

type internal GoToDefinition(checker: FSharpChecker, projectInfoManager: FSharpProjectOptionsManager) =
let userOpName = "GoToDefinition"

Expand Down Expand Up @@ -241,23 +255,28 @@ type internal GoToDefinition(checker: FSharpChecker, projectInfoManager: FSharpP

match declarations with
| FSharpFindDeclResult.ExternalDecl (assembly, targetExternalSym) ->
let! project = originDocument.Project.Solution.Projects |> Seq.tryFind (fun p -> p.AssemblyName.Equals(assembly, StringComparison.OrdinalIgnoreCase))
let! symbols = SymbolFinder.FindSourceDeclarationsAsync(project, fun _ -> true)

let roslynSymbols =
symbols
|> Seq.collect ExternalSymbol.ofRoslynSymbol
|> Array.ofSeq

let! symbol =
roslynSymbols
|> Seq.tryPick (fun (sym, externalSym) ->
if externalSym = targetExternalSym then Some sym
else None
)

let! location = symbol.Locations |> Seq.tryHead
return (FSharpGoToDefinitionNavigableItem(project.GetDocument(location.SourceTree), location.SourceSpan), idRange)
let projectOpt = originDocument.Project.Solution.Projects |> Seq.tryFind (fun p -> p.AssemblyName.Equals(assembly, StringComparison.OrdinalIgnoreCase))
match projectOpt with
| Some project ->
let! symbols = SymbolFinder.FindSourceDeclarationsAsync(project, fun _ -> true)

let roslynSymbols =
symbols
|> Seq.collect ExternalSymbol.ofRoslynSymbol
|> Array.ofSeq

let! symbol =
roslynSymbols
|> Seq.tryPick (fun (sym, externalSym) ->
if externalSym = targetExternalSym then Some sym
else None
)

let! location = symbol.Locations |> Seq.tryHead
return (FSharpGoToDefinitionResult.NavigableItem(FSharpGoToDefinitionNavigableItem(project.GetDocument(location.SourceTree), location.SourceSpan)), idRange)
| _ ->
let tmpProjInfo, tmpDocId = MetadataAsSource.generateTemporaryCSharpDocument(AssemblyIdentity(targetSymbolUse.Symbol.Assembly.QualifiedName), targetSymbolUse.Symbol.DisplayName, originDocument.Project.MetadataReferences)
return (FSharpGoToDefinitionResult.ExternalAssembly(tmpProjInfo, tmpDocId, targetSymbolUse, targetExternalSym), idRange)

| FSharpFindDeclResult.DeclFound targetRange ->
// if goto definition is called at we are alread at the declaration location of a symbol in
Expand All @@ -275,7 +294,7 @@ type internal GoToDefinition(checker: FSharpChecker, projectInfoManager: FSharpP

let! implTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (implSourceText, targetRange)
let navItem = FSharpGoToDefinitionNavigableItem (implDocument, implTextSpan)
return (navItem, idRange)
return (FSharpGoToDefinitionResult.NavigableItem(navItem), idRange)
else // jump from implementation to the corresponding signature
let declarations = checkFileResults.GetDeclarationLocation (fcsTextLineNumber, lexerSymbol.Ident.idRange.EndColumn, textLineString, lexerSymbol.FullIsland, true)
match declarations with
Expand All @@ -284,7 +303,7 @@ type internal GoToDefinition(checker: FSharpChecker, projectInfoManager: FSharpP
let! sigSourceText = sigDocument.GetTextAsync () |> liftTaskAsync
let! sigTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (sigSourceText, targetRange)
let navItem = FSharpGoToDefinitionNavigableItem (sigDocument, sigTextSpan)
return (navItem, idRange)
return (FSharpGoToDefinitionResult.NavigableItem(navItem), idRange)
| _ ->
return! None
// when the target range is different follow the navigation convention of
Expand All @@ -297,7 +316,7 @@ type internal GoToDefinition(checker: FSharpChecker, projectInfoManager: FSharpP
// if the gotodef call originated from a signature and the returned target is a signature, navigate there
if isSignatureFile targetRange.FileName && preferSignature then
let navItem = FSharpGoToDefinitionNavigableItem (sigDocument, sigTextSpan)
return (navItem, idRange)
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 =
Expand All @@ -314,7 +333,7 @@ type internal GoToDefinition(checker: FSharpChecker, projectInfoManager: FSharpP

let! implTextSpan = RoslynHelpers.TryFSharpRangeToTextSpan (implSourceText, targetRange)
let navItem = FSharpGoToDefinitionNavigableItem (implDocument, implTextSpan)
return (navItem, idRange)
return (FSharpGoToDefinitionResult.NavigableItem(navItem), idRange)
| _ ->
return! None
}
Expand All @@ -330,8 +349,7 @@ type internal GoToDefinition(checker: FSharpChecker, projectInfoManager: FSharpP
member this.FindDefinitionsForPeekTask(originDocument: Document, position: int, cancellationToken: CancellationToken) =
this.FindDefinitionAtPosition(originDocument, position)
|> Async.map (
Option.map (fun (navItem, _) -> navItem :> FSharpNavigableItem)
>> Option.toArray
Option.toArray
>> Array.toSeq)
|> RoslynHelpers.StartAsyncAsTask cancellationToken

Expand All @@ -343,7 +361,7 @@ type internal GoToDefinition(checker: FSharpChecker, projectInfoManager: FSharpP

/// Navigate to the positon of the textSpan in the provided document
/// used by quickinfo link navigation when the tooltip contains the correct destination range.
member _.TryNavigateToTextSpan(document: Document, textSpan: TextSpan, statusBar: StatusBar) =
member _.TryNavigateToTextSpan(document: Document, textSpan: Microsoft.CodeAnalysis.Text.TextSpan, statusBar: StatusBar) =
let navigableItem = FSharpGoToDefinitionNavigableItem(document, textSpan)
let workspace = document.Project.Solution.Workspace
let navigationService = workspace.Services.GetService<IFSharpDocumentNavigationService>()
Expand Down
Loading