From a9bd7f30f41cd310eb40cfccfff7f6c59c4b314f Mon Sep 17 00:00:00 2001 From: Omar Tawfik Date: Wed, 14 Sep 2016 17:05:18 -0700 Subject: [PATCH 1/3] Added FSharpGoToDefinitionService --- .../src/FSharp.Editor/FSharp.Editor.fsproj | 3 + .../FSharp.Editor/GoToDefinitionService.fs | 134 ++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 vsintegration/src/FSharp.Editor/GoToDefinitionService.fs diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index 9235da55b97..f304c930e6f 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -64,6 +64,9 @@ Completion\CompletionServiceFactory.fs + + GoToDefinition\GoToDefinitionService.fs + diff --git a/vsintegration/src/FSharp.Editor/GoToDefinitionService.fs b/vsintegration/src/FSharp.Editor/GoToDefinitionService.fs new file mode 100644 index 00000000000..f191051bf24 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/GoToDefinitionService.fs @@ -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.Empty + +[] +[, FSharpCommonConstants.FSharpLanguageName)>] +type internal FSharpGoToDefinitionService [] ([]presenters: IEnumerable) = + + static member FindDefinition (sourceText: SourceText, + filePath: string, + position: int, + defines: string list, + options: FSharpProjectOptions, + textVersionHash: int, + cancellationToken: CancellationToken) + : Async> = 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() + 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 From c0864353e8b5fdb923eb57b05ad8a648205b9664 Mon Sep 17 00:00:00 2001 From: Omar Tawfik Date: Thu, 15 Sep 2016 11:10:39 -0700 Subject: [PATCH 2/3] Added GoToDefinitionServiceTests --- .../unittests/GoToDefinitionServiceTests.fs | 68 +++++++++++++++++++ .../unittests/VisualFSharp.Unittests.fsproj | 3 + 2 files changed, 71 insertions(+) create mode 100644 vsintegration/tests/unittests/GoToDefinitionServiceTests.fs diff --git a/vsintegration/tests/unittests/GoToDefinitionServiceTests.fs b/vsintegration/tests/unittests/GoToDefinitionServiceTests.fs new file mode 100644 index 00000000000..68ff3c3fc01 --- /dev/null +++ b/vsintegration/tests/unittests/GoToDefinitionServiceTests.fs @@ -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 + +[] +type GoToDefinitionServiceTests() = + + [] + [] + [] + [] + [] + [] + 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 + +[] +let main argv = + let obj = TestType() + obj.Member1(5) + obj.Member2("test")""" + + let filePath = Path.GetTempFileName() + ".fs" + let options: FSharpProjectOptions = { + ProjectFileName = "C:\\test.fsproj" + 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") diff --git a/vsintegration/tests/unittests/VisualFSharp.Unittests.fsproj b/vsintegration/tests/unittests/VisualFSharp.Unittests.fsproj index f0f3a8361fc..f1c06ce344a 100644 --- a/vsintegration/tests/unittests/VisualFSharp.Unittests.fsproj +++ b/vsintegration/tests/unittests/VisualFSharp.Unittests.fsproj @@ -55,6 +55,9 @@ Roslyn\Completion\CompletionProviderTests.fs + + Roslyn\GoToDefinition\GoToDefinitionServiceTests.fs + From 0125c714703c4b55e20b2a582610ab9f9d3ca628 Mon Sep 17 00:00:00 2001 From: Omar Tawfik Date: Tue, 20 Sep 2016 17:30:00 -0700 Subject: [PATCH 3/3] Remove VsMocks Microsoft.VisualStudio.Platform.VSEditor.Interop dependency --- vsintegration/tests/Salsa/VsMocks.fs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/vsintegration/tests/Salsa/VsMocks.fs b/vsintegration/tests/Salsa/VsMocks.fs index dfc6196eaee..577de5c5100 100644 --- a/vsintegration/tests/Salsa/VsMocks.fs +++ b/vsintegration/tests/Salsa/VsMocks.fs @@ -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() list.Add(CreateAssemblyCatalog(root, "Microsoft.VisualStudio.Platform.VSEditor.dll"))