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
3 changes: 3 additions & 0 deletions vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@
<Compile Include="CompletionServiceFactory.fs">
<Link>Completion\CompletionServiceFactory.fs</Link>
</Compile>
<Compile Include="GoToDefinitionService.fs">
<Link>GoToDefinition\GoToDefinitionService.fs</Link>
</Compile>
<Compile Include="ContentType.fs" />
</ItemGroup>
<ItemGroup>
Expand Down
134 changes: 134 additions & 0 deletions vsintegration/src/FSharp.Editor/GoToDefinitionService.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

namespace Microsoft.VisualStudio.FSharp.Editor

open System
open System.Composition
open System.Collections.Concurrent
open System.Collections.Generic
open System.Collections.Immutable
open System.Linq
open System.Threading
open System.Threading.Tasks
open System.Runtime.CompilerServices

open Microsoft.CodeAnalysis
open Microsoft.CodeAnalysis.Classification
open Microsoft.CodeAnalysis.Editor
open Microsoft.CodeAnalysis.Editor.Host
open Microsoft.CodeAnalysis.Editor.Navigation
open Microsoft.CodeAnalysis.Editor.Shared.Utilities
open Microsoft.CodeAnalysis.Host.Mef
open Microsoft.CodeAnalysis.Text

open Microsoft.VisualStudio.FSharp.LanguageService
open Microsoft.VisualStudio.Text
open Microsoft.VisualStudio.Text.Tagging

open Microsoft.FSharp.Compiler.Range
open Microsoft.FSharp.Compiler.SourceCodeServices

type internal FSharpNavigableItem(document: Document, textSpan: TextSpan, displayString: string) =
interface INavigableItem with
member this.Glyph = Glyph.BasicFile
member this.DisplayFileLocation = true
member this.DisplayString = displayString
member this.Document = document
member this.SourceSpan = textSpan
member this.ChildItems = ImmutableArray<INavigableItem>.Empty

[<Shared>]
[<ExportLanguageService(typeof<IGoToDefinitionService>, FSharpCommonConstants.FSharpLanguageName)>]
type internal FSharpGoToDefinitionService [<ImportingConstructor>] ([<ImportMany>]presenters: IEnumerable<INavigableItemsPresenter>) =

static member FindDefinition (sourceText: SourceText,
filePath: string,
position: int,
defines: string list,
options: FSharpProjectOptions,
textVersionHash: int,
cancellationToken: CancellationToken)
: Async<Option<range>> = async {

let textLine = sourceText.Lines.GetLineFromPosition(position)
let textLineNumber = textLine.LineNumber + 1 // Roslyn line numbers are zero-based
let textLineColumn = sourceText.Lines.GetLinePosition(position).Character
let classifiedSpanOption =
FSharpColorizationService.GetColorizationData(sourceText, textLine.Span, Some(filePath), defines, cancellationToken)
|> Seq.tryFind(fun classifiedSpan -> classifiedSpan.TextSpan.Contains(position))

let processQualifiedIdentifier(qualifiers, islandColumn) = async {
let! parseResults = FSharpChecker.Instance.ParseFileInProject(filePath, sourceText.ToString(), options)
let! checkFileAnswer = FSharpChecker.Instance.CheckFileInProject(parseResults, filePath, textVersionHash, sourceText.ToString(), options)
let checkFileResults = match checkFileAnswer with
| FSharpCheckFileAnswer.Aborted -> failwith "Compilation isn't complete yet"
| FSharpCheckFileAnswer.Succeeded(results) -> results

let! declarations = checkFileResults.GetDeclarationLocationAlternate (textLineNumber, islandColumn, textLine.ToString(), qualifiers, false)

return match declarations with
| FSharpFindDeclResult.DeclFound(range) -> Some(range)
| _ -> None
}

return match classifiedSpanOption with
| Some(classifiedSpan) ->
match classifiedSpan.ClassificationType with
| ClassificationTypeNames.Identifier ->
match QuickParse.GetCompleteIdentifierIsland true (textLine.ToString()) textLineColumn with
| Some(islandIdentifier, islandColumn, isQuoted) ->
let qualifiers = if isQuoted then [islandIdentifier] else islandIdentifier.Split '.' |> Array.toList
processQualifiedIdentifier(qualifiers, islandColumn) |> Async.RunSynchronously
| None -> None
| _ -> None
| None -> None
}

// FSROSLYNTODO: Since we are not integrated with the Roslyn project system yet, the below call
// document.Project.Solution.GetDocumentIdsWithFilePath() will only access files in the same project.
// Either Roslyn INavigableItem needs to be extended to allow arbitary full paths, or we need to
// fully integrate with their project system.
member this.FindDefinitionsAsyncAux(document: Document, position: int, cancellationToken: CancellationToken) =
let computation = async {
let results = List<INavigableItem>()
match FSharpLanguageService.GetOptions(document.Project.Id) with
| Some(options) ->
let! sourceText = document.GetTextAsync(cancellationToken) |> Async.AwaitTask
let! textVersion = document.GetTextVersionAsync(cancellationToken) |> Async.AwaitTask
let defines = CompilerEnvironment.GetCompilationDefinesForEditing(document.Name, options.OtherOptions |> Seq.toList)
let! definition = FSharpGoToDefinitionService.FindDefinition(sourceText, document.FilePath, position, defines, options, textVersion.GetHashCode(), cancellationToken)

match definition with
| Some(range) ->
let refDocumentId = document.Project.Solution.GetDocumentIdsWithFilePath(range.FileName).First()
let refDocument = document.Project.Solution.GetDocument(refDocumentId)
let! refSourceText = refDocument.GetTextAsync(cancellationToken) |> Async.AwaitTask
let refTextSpan = CommonRoslynHelpers.FSharpRangeToTextSpan(refSourceText, range)
let refDisplayString = refSourceText.GetSubText(refTextSpan).ToString()
results.Add(FSharpNavigableItem(refDocument, refTextSpan, refDisplayString))
| None -> ()
| None -> ()
return results.AsEnumerable()
}

Async.StartAsTask(computation, TaskCreationOptions.None, cancellationToken)
.ContinueWith(CommonRoslynHelpers.GetCompletedTaskResult, cancellationToken)

interface IGoToDefinitionService with
member this.FindDefinitionsAsync(document: Document, position: int, cancellationToken: CancellationToken) =
this.FindDefinitionsAsyncAux(document, position, cancellationToken)

member this.TryGoToDefinition(document: Document, position: int, cancellationToken: CancellationToken) =
let definitionTask = this.FindDefinitionsAsyncAux(document, position, cancellationToken)
definitionTask.Wait(cancellationToken)

if definitionTask.Status = TaskStatus.RanToCompletion then
if definitionTask.Result.Any() then
let navigableItem = definitionTask.Result.First() // F# API provides only one INavigableItem
for presenter in presenters do
presenter.DisplayResult(navigableItem.DisplayString, definitionTask.Result)
true
else
false
else
false
11 changes: 0 additions & 11 deletions vsintegration/tests/Salsa/VsMocks.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1626,17 +1626,6 @@ module internal VsActual =
else
failwith("could not find " + fullPath)

// copy this private assembly next to unit tests, otherwise assembly loader cannot find it
let neededLocalAssem = Path.Combine(vsInstallDir, @"IDE\PrivateAssemblies\Microsoft.VisualStudio.Platform.VSEditor.Interop.dll")

#if NUNIT_2
let curDir = Path.GetDirectoryName((new System.Uri(System.Reflection.Assembly.Load("nunit.util").EscapedCodeBase)).LocalPath)
#else
let curDir = Path.GetDirectoryName((new System.Uri(System.Reflection.Assembly.Load("nunit.framework").EscapedCodeBase)).LocalPath)
#endif
let localCopy = Path.Combine(curDir, System.IO.Path.GetFileName(neededLocalAssem))
System.IO.File.Copy(neededLocalAssem, localCopy, true)

let list = new ResizeArray<ComposablePartCatalog>()
list.Add(CreateAssemblyCatalog(root, "Microsoft.VisualStudio.Platform.VSEditor.dll"))

Expand Down
68 changes: 68 additions & 0 deletions vsintegration/tests/unittests/GoToDefinitionServiceTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.VisualStudio.FSharp.Editor.Tests.Roslyn

open System
open System.IO
open System.Threading
open System.Linq

open NUnit.Framework

open Microsoft.CodeAnalysis.Completion
open Microsoft.CodeAnalysis.Classification
open Microsoft.CodeAnalysis.Text
open Microsoft.VisualStudio.FSharp.Editor

open Microsoft.VisualStudio.FSharp.Editor
open Microsoft.VisualStudio.FSharp.LanguageService

open Microsoft.FSharp.Compiler.SourceCodeServices
open Microsoft.FSharp.Compiler.Range

[<TestFixture>]
type GoToDefinitionServiceTests() =

[<TestCase("printf \"%d\" par1", 3, 24, 28)>]
[<TestCase("printf \"%s\" par2", 5, 24, 28)>]
[<TestCase("let obj = TestType", 2, 5, 13)>]
[<TestCase("let obj", 10, 8, 11)>]
[<TestCase("obj.Member1", 3, 16, 23)>]
[<TestCase("obj.Member2", 5, 16, 23)>]
member this.VerifyDefinition(caretMarker: string, definitionLine: int, definitionStartColumn: int, definitionEndColumn: int) =
let fileContents = """
type TestType() =
member this.Member1(par1: int) =
printf "%d" par1
member this.Member2(par2: string) =
printf "%s" par2

[<EntryPoint>]
let main argv =
let obj = TestType()
obj.Member1(5)
obj.Member2("test")"""

let filePath = Path.GetTempFileName() + ".fs"
let options: FSharpProjectOptions = {
ProjectFileName = "C:\\test.fsproj"
Copy link
Contributor

@KevinRansom KevinRansom Sep 16, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c:\test.fsproj is that what we want to do here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a placeholder to create a dummy object to pass to the service.
Actual compiler service tests are in Tests.LanguageService.GotoDefinition.fs. This file contains the tests that target the Roslyn service export.

ProjectFileNames = [| filePath |]
ReferencedProjects = [| |]
OtherOptions = [| |]
IsIncompleteTypeCheckEnvironment = true
UseScriptResolutionRules = false
LoadTime = DateTime.MaxValue
UnresolvedReferences = None
}

File.WriteAllText(filePath, fileContents)

let caretPosition = fileContents.IndexOf(caretMarker) + caretMarker.Length - 1 // inside the marker
let definitionOption = FSharpGoToDefinitionService.FindDefinition(SourceText.From(fileContents), filePath, caretPosition, [], options, 0, CancellationToken.None) |> Async.RunSynchronously

match definitionOption with
| None -> Assert.Fail("No definition found")
| Some(range) ->
Assert.AreEqual(range.StartLine, range.EndLine, "Range must be on the same line")
Assert.AreEqual(definitionLine, range.StartLine, "Range line should match")
Assert.AreEqual(definitionStartColumn, range.StartColumn, "Range start column should match")
Assert.AreEqual(definitionEndColumn, range.EndColumn, "Range end column should match")
3 changes: 3 additions & 0 deletions vsintegration/tests/unittests/VisualFSharp.Unittests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@
<Compile Include="CompletionProviderTests.fs">
<Link>Roslyn\Completion\CompletionProviderTests.fs</Link>
</Compile>
<Compile Include="GoToDefinitionServiceTests.fs">
<Link>Roslyn\GoToDefinition\GoToDefinitionServiceTests.fs</Link>
</Compile>
<Compile Include="Tests.InternalCollections.fs" />
<Compile Include="Tests.Build.fs" />
<Compile Include="Tests.TaskReporter.fs" />
Expand Down