Skip to content

Commit d55f838

Browse files
authored
Merge pull request #1540 from OmarTawfik/roslyn-gotodef
Roslyn GoToDefinition Service Integration
2 parents fd874f2 + 0125c71 commit d55f838

File tree

5 files changed

+208
-11
lines changed

5 files changed

+208
-11
lines changed

vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@
6464
<Compile Include="CompletionServiceFactory.fs">
6565
<Link>Completion\CompletionServiceFactory.fs</Link>
6666
</Compile>
67+
<Compile Include="GoToDefinitionService.fs">
68+
<Link>GoToDefinition\GoToDefinitionService.fs</Link>
69+
</Compile>
6770
<Compile Include="ContentType.fs" />
6871
</ItemGroup>
6972
<ItemGroup>
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// 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.
2+
3+
namespace Microsoft.VisualStudio.FSharp.Editor
4+
5+
open System
6+
open System.Composition
7+
open System.Collections.Concurrent
8+
open System.Collections.Generic
9+
open System.Collections.Immutable
10+
open System.Linq
11+
open System.Threading
12+
open System.Threading.Tasks
13+
open System.Runtime.CompilerServices
14+
15+
open Microsoft.CodeAnalysis
16+
open Microsoft.CodeAnalysis.Classification
17+
open Microsoft.CodeAnalysis.Editor
18+
open Microsoft.CodeAnalysis.Editor.Host
19+
open Microsoft.CodeAnalysis.Editor.Navigation
20+
open Microsoft.CodeAnalysis.Editor.Shared.Utilities
21+
open Microsoft.CodeAnalysis.Host.Mef
22+
open Microsoft.CodeAnalysis.Text
23+
24+
open Microsoft.VisualStudio.FSharp.LanguageService
25+
open Microsoft.VisualStudio.Text
26+
open Microsoft.VisualStudio.Text.Tagging
27+
28+
open Microsoft.FSharp.Compiler.Range
29+
open Microsoft.FSharp.Compiler.SourceCodeServices
30+
31+
type internal FSharpNavigableItem(document: Document, textSpan: TextSpan, displayString: string) =
32+
interface INavigableItem with
33+
member this.Glyph = Glyph.BasicFile
34+
member this.DisplayFileLocation = true
35+
member this.DisplayString = displayString
36+
member this.Document = document
37+
member this.SourceSpan = textSpan
38+
member this.ChildItems = ImmutableArray<INavigableItem>.Empty
39+
40+
[<Shared>]
41+
[<ExportLanguageService(typeof<IGoToDefinitionService>, FSharpCommonConstants.FSharpLanguageName)>]
42+
type internal FSharpGoToDefinitionService [<ImportingConstructor>] ([<ImportMany>]presenters: IEnumerable<INavigableItemsPresenter>) =
43+
44+
static member FindDefinition (sourceText: SourceText,
45+
filePath: string,
46+
position: int,
47+
defines: string list,
48+
options: FSharpProjectOptions,
49+
textVersionHash: int,
50+
cancellationToken: CancellationToken)
51+
: Async<Option<range>> = async {
52+
53+
let textLine = sourceText.Lines.GetLineFromPosition(position)
54+
let textLineNumber = textLine.LineNumber + 1 // Roslyn line numbers are zero-based
55+
let textLineColumn = sourceText.Lines.GetLinePosition(position).Character
56+
let classifiedSpanOption =
57+
FSharpColorizationService.GetColorizationData(sourceText, textLine.Span, Some(filePath), defines, cancellationToken)
58+
|> Seq.tryFind(fun classifiedSpan -> classifiedSpan.TextSpan.Contains(position))
59+
60+
let processQualifiedIdentifier(qualifiers, islandColumn) = async {
61+
let! parseResults = FSharpChecker.Instance.ParseFileInProject(filePath, sourceText.ToString(), options)
62+
let! checkFileAnswer = FSharpChecker.Instance.CheckFileInProject(parseResults, filePath, textVersionHash, sourceText.ToString(), options)
63+
let checkFileResults = match checkFileAnswer with
64+
| FSharpCheckFileAnswer.Aborted -> failwith "Compilation isn't complete yet"
65+
| FSharpCheckFileAnswer.Succeeded(results) -> results
66+
67+
let! declarations = checkFileResults.GetDeclarationLocationAlternate (textLineNumber, islandColumn, textLine.ToString(), qualifiers, false)
68+
69+
return match declarations with
70+
| FSharpFindDeclResult.DeclFound(range) -> Some(range)
71+
| _ -> None
72+
}
73+
74+
return match classifiedSpanOption with
75+
| Some(classifiedSpan) ->
76+
match classifiedSpan.ClassificationType with
77+
| ClassificationTypeNames.Identifier ->
78+
match QuickParse.GetCompleteIdentifierIsland true (textLine.ToString()) textLineColumn with
79+
| Some(islandIdentifier, islandColumn, isQuoted) ->
80+
let qualifiers = if isQuoted then [islandIdentifier] else islandIdentifier.Split '.' |> Array.toList
81+
processQualifiedIdentifier(qualifiers, islandColumn) |> Async.RunSynchronously
82+
| None -> None
83+
| _ -> None
84+
| None -> None
85+
}
86+
87+
// FSROSLYNTODO: Since we are not integrated with the Roslyn project system yet, the below call
88+
// document.Project.Solution.GetDocumentIdsWithFilePath() will only access files in the same project.
89+
// Either Roslyn INavigableItem needs to be extended to allow arbitary full paths, or we need to
90+
// fully integrate with their project system.
91+
member this.FindDefinitionsAsyncAux(document: Document, position: int, cancellationToken: CancellationToken) =
92+
let computation = async {
93+
let results = List<INavigableItem>()
94+
match FSharpLanguageService.GetOptions(document.Project.Id) with
95+
| Some(options) ->
96+
let! sourceText = document.GetTextAsync(cancellationToken) |> Async.AwaitTask
97+
let! textVersion = document.GetTextVersionAsync(cancellationToken) |> Async.AwaitTask
98+
let defines = CompilerEnvironment.GetCompilationDefinesForEditing(document.Name, options.OtherOptions |> Seq.toList)
99+
let! definition = FSharpGoToDefinitionService.FindDefinition(sourceText, document.FilePath, position, defines, options, textVersion.GetHashCode(), cancellationToken)
100+
101+
match definition with
102+
| Some(range) ->
103+
let refDocumentId = document.Project.Solution.GetDocumentIdsWithFilePath(range.FileName).First()
104+
let refDocument = document.Project.Solution.GetDocument(refDocumentId)
105+
let! refSourceText = refDocument.GetTextAsync(cancellationToken) |> Async.AwaitTask
106+
let refTextSpan = CommonRoslynHelpers.FSharpRangeToTextSpan(refSourceText, range)
107+
let refDisplayString = refSourceText.GetSubText(refTextSpan).ToString()
108+
results.Add(FSharpNavigableItem(refDocument, refTextSpan, refDisplayString))
109+
| None -> ()
110+
| None -> ()
111+
return results.AsEnumerable()
112+
}
113+
114+
Async.StartAsTask(computation, TaskCreationOptions.None, cancellationToken)
115+
.ContinueWith(CommonRoslynHelpers.GetCompletedTaskResult, cancellationToken)
116+
117+
interface IGoToDefinitionService with
118+
member this.FindDefinitionsAsync(document: Document, position: int, cancellationToken: CancellationToken) =
119+
this.FindDefinitionsAsyncAux(document, position, cancellationToken)
120+
121+
member this.TryGoToDefinition(document: Document, position: int, cancellationToken: CancellationToken) =
122+
let definitionTask = this.FindDefinitionsAsyncAux(document, position, cancellationToken)
123+
definitionTask.Wait(cancellationToken)
124+
125+
if definitionTask.Status = TaskStatus.RanToCompletion then
126+
if definitionTask.Result.Any() then
127+
let navigableItem = definitionTask.Result.First() // F# API provides only one INavigableItem
128+
for presenter in presenters do
129+
presenter.DisplayResult(navigableItem.DisplayString, definitionTask.Result)
130+
true
131+
else
132+
false
133+
else
134+
false

vsintegration/tests/Salsa/VsMocks.fs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1626,17 +1626,6 @@ module internal VsActual =
16261626
else
16271627
failwith("could not find " + fullPath)
16281628

1629-
// copy this private assembly next to unit tests, otherwise assembly loader cannot find it
1630-
let neededLocalAssem = Path.Combine(vsInstallDir, @"IDE\PrivateAssemblies\Microsoft.VisualStudio.Platform.VSEditor.Interop.dll")
1631-
1632-
#if NUNIT_2
1633-
let curDir = Path.GetDirectoryName((new System.Uri(System.Reflection.Assembly.Load("nunit.util").EscapedCodeBase)).LocalPath)
1634-
#else
1635-
let curDir = Path.GetDirectoryName((new System.Uri(System.Reflection.Assembly.Load("nunit.framework").EscapedCodeBase)).LocalPath)
1636-
#endif
1637-
let localCopy = Path.Combine(curDir, System.IO.Path.GetFileName(neededLocalAssem))
1638-
System.IO.File.Copy(neededLocalAssem, localCopy, true)
1639-
16401629
let list = new ResizeArray<ComposablePartCatalog>()
16411630
list.Add(CreateAssemblyCatalog(root, "Microsoft.VisualStudio.Platform.VSEditor.dll"))
16421631

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// 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.
2+
namespace Microsoft.VisualStudio.FSharp.Editor.Tests.Roslyn
3+
4+
open System
5+
open System.IO
6+
open System.Threading
7+
open System.Linq
8+
9+
open NUnit.Framework
10+
11+
open Microsoft.CodeAnalysis.Completion
12+
open Microsoft.CodeAnalysis.Classification
13+
open Microsoft.CodeAnalysis.Text
14+
open Microsoft.VisualStudio.FSharp.Editor
15+
16+
open Microsoft.VisualStudio.FSharp.Editor
17+
open Microsoft.VisualStudio.FSharp.LanguageService
18+
19+
open Microsoft.FSharp.Compiler.SourceCodeServices
20+
open Microsoft.FSharp.Compiler.Range
21+
22+
[<TestFixture>]
23+
type GoToDefinitionServiceTests() =
24+
25+
[<TestCase("printf \"%d\" par1", 3, 24, 28)>]
26+
[<TestCase("printf \"%s\" par2", 5, 24, 28)>]
27+
[<TestCase("let obj = TestType", 2, 5, 13)>]
28+
[<TestCase("let obj", 10, 8, 11)>]
29+
[<TestCase("obj.Member1", 3, 16, 23)>]
30+
[<TestCase("obj.Member2", 5, 16, 23)>]
31+
member this.VerifyDefinition(caretMarker: string, definitionLine: int, definitionStartColumn: int, definitionEndColumn: int) =
32+
let fileContents = """
33+
type TestType() =
34+
member this.Member1(par1: int) =
35+
printf "%d" par1
36+
member this.Member2(par2: string) =
37+
printf "%s" par2
38+
39+
[<EntryPoint>]
40+
let main argv =
41+
let obj = TestType()
42+
obj.Member1(5)
43+
obj.Member2("test")"""
44+
45+
let filePath = Path.GetTempFileName() + ".fs"
46+
let options: FSharpProjectOptions = {
47+
ProjectFileName = "C:\\test.fsproj"
48+
ProjectFileNames = [| filePath |]
49+
ReferencedProjects = [| |]
50+
OtherOptions = [| |]
51+
IsIncompleteTypeCheckEnvironment = true
52+
UseScriptResolutionRules = false
53+
LoadTime = DateTime.MaxValue
54+
UnresolvedReferences = None
55+
}
56+
57+
File.WriteAllText(filePath, fileContents)
58+
59+
let caretPosition = fileContents.IndexOf(caretMarker) + caretMarker.Length - 1 // inside the marker
60+
let definitionOption = FSharpGoToDefinitionService.FindDefinition(SourceText.From(fileContents), filePath, caretPosition, [], options, 0, CancellationToken.None) |> Async.RunSynchronously
61+
62+
match definitionOption with
63+
| None -> Assert.Fail("No definition found")
64+
| Some(range) ->
65+
Assert.AreEqual(range.StartLine, range.EndLine, "Range must be on the same line")
66+
Assert.AreEqual(definitionLine, range.StartLine, "Range line should match")
67+
Assert.AreEqual(definitionStartColumn, range.StartColumn, "Range start column should match")
68+
Assert.AreEqual(definitionEndColumn, range.EndColumn, "Range end column should match")

vsintegration/tests/unittests/VisualFSharp.Unittests.fsproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@
5555
<Compile Include="CompletionProviderTests.fs">
5656
<Link>Roslyn\Completion\CompletionProviderTests.fs</Link>
5757
</Compile>
58+
<Compile Include="GoToDefinitionServiceTests.fs">
59+
<Link>Roslyn\GoToDefinition\GoToDefinitionServiceTests.fs</Link>
60+
</Compile>
5861
<Compile Include="Tests.InternalCollections.fs" />
5962
<Compile Include="Tests.Build.fs" />
6063
<Compile Include="Tests.TaskReporter.fs" />

0 commit comments

Comments
 (0)