-
Notifications
You must be signed in to change notification settings - Fork 789
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement DocumentHighlightsService (#1943)
* implement DocumentHighlightsService * use the right ImportingConstructor attribute * tolerate being on the right of the identifier * add a DocumentHighlightsService test * fix erroneous qualified symbol ranges (wip) * fix `fixInvalidSymbolSpans` * fix after merge * update Microsoft.CodeAnalysis.xxx packages to 2.0.0-rc * remove double try to find symbol * fix after merge fix tryClassifyAtPosition
- Loading branch information
1 parent
a0014ed
commit 6dd77c7
Showing
7 changed files
with
272 additions
and
38 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
115 changes: 115 additions & 0 deletions
115
vsintegration/src/FSharp.Editor/DocumentHighlights/DocumentHighlightsService.fs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
// 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.Threading | ||
open System.Threading.Tasks | ||
open System.Linq | ||
open System.Runtime.CompilerServices | ||
|
||
open Microsoft.CodeAnalysis | ||
open Microsoft.CodeAnalysis.Completion | ||
open Microsoft.CodeAnalysis.Classification | ||
open Microsoft.CodeAnalysis.Editor | ||
open Microsoft.CodeAnalysis.Editor.Implementation.ReferenceHighlighting | ||
open Microsoft.CodeAnalysis.Host.Mef | ||
open Microsoft.CodeAnalysis.Options | ||
open Microsoft.CodeAnalysis.Text | ||
|
||
open Microsoft.VisualStudio.FSharp.LanguageService | ||
open Microsoft.VisualStudio.Text | ||
open Microsoft.VisualStudio.Text.Classification | ||
open Microsoft.VisualStudio.Text.Tagging | ||
|
||
open Microsoft.FSharp.Compiler | ||
open Microsoft.FSharp.Compiler.Parser | ||
open Microsoft.FSharp.Compiler.Range | ||
open Microsoft.FSharp.Compiler.SourceCodeServices | ||
open System.Windows.Documents | ||
|
||
type internal FSharpHighlightSpan = | ||
{ IsDefinition: bool | ||
TextSpan: TextSpan } | ||
override this.ToString() = sprintf "%+A" this | ||
|
||
[<Shared>] | ||
[<ExportLanguageService(typeof<IDocumentHighlightsService>, FSharpCommonConstants.FSharpLanguageName)>] | ||
type internal FSharpDocumentHighlightsService [<ImportingConstructor>] (checkerProvider: FSharpCheckerProvider, projectInfoManager: ProjectInfoManager) = | ||
|
||
/// Fix invalid spans if they appear to have redundant suffix and prefix. | ||
static let fixInvalidSymbolSpans (sourceText: SourceText) (lastIdent: string) (spans: FSharpHighlightSpan []) = | ||
spans | ||
|> Seq.choose (fun (span: FSharpHighlightSpan) -> | ||
let newLastIdent = sourceText.GetSubText(span.TextSpan).ToString() | ||
let index = newLastIdent.LastIndexOf(lastIdent, StringComparison.Ordinal) | ||
if index > 0 then | ||
// Sometimes FCS returns a composite identifier for a short symbol, so we truncate the prefix | ||
// Example: newLastIdent --> "x.Length", lastIdent --> "Length" | ||
Some { span with TextSpan = TextSpan(span.TextSpan.Start + index, span.TextSpan.Length - index) } | ||
elif index = 0 && newLastIdent.Length > lastIdent.Length then | ||
// The returned symbol use is too long; we truncate its redundant suffix | ||
// Example: newLastIdent --> "Length<'T>", lastIdent --> "Length" | ||
Some { span with TextSpan = TextSpan(span.TextSpan.Start, lastIdent.Length) } | ||
elif index = 0 then | ||
Some span | ||
else | ||
// In the case of attributes, a returned symbol use may be a part of original text | ||
// Example: newLastIdent --> "Sample", lastIdent --> "SampleAttribute" | ||
let index = lastIdent.LastIndexOf(newLastIdent, StringComparison.Ordinal) | ||
if index >= 0 then | ||
Some span | ||
else None) | ||
|> Seq.distinctBy (fun span -> span.TextSpan.Start) | ||
|> Seq.toArray | ||
|
||
static member GetDocumentHighlights(checker: FSharpChecker, documentKey: DocumentId, sourceText: SourceText, filePath: string, position: int, | ||
defines: string list, options: FSharpProjectOptions, textVersionHash: int, cancellationToken: CancellationToken) : Async<FSharpHighlightSpan[]> = | ||
async { | ||
let textLine = sourceText.Lines.GetLineFromPosition(position) | ||
let textLinePos = sourceText.Lines.GetLinePosition(position) | ||
let fcsTextLineNumber = textLinePos.Line + 1 | ||
|
||
match CommonHelpers.tryClassifyAtPosition(documentKey, sourceText, filePath, defines, position, SymbolSearchKind.IncludeRightColumn, cancellationToken) with | ||
| Some (_, [], _) -> return [||] | ||
| Some (islandEndColumn, qualifiers, _span) -> | ||
let! _parseResults, checkFileAnswer = checker.ParseAndCheckFileInProject(filePath, textVersionHash, sourceText.ToString(), options) | ||
match checkFileAnswer with | ||
| FSharpCheckFileAnswer.Aborted -> return [||] | ||
| FSharpCheckFileAnswer.Succeeded(checkFileResults) -> | ||
let! symbolUse = checkFileResults.GetSymbolUseAtLocation(fcsTextLineNumber, islandEndColumn, textLine.ToString(), qualifiers) | ||
match symbolUse with | ||
| Some symbolUse -> | ||
let! symbolUses = checkFileResults.GetUsesOfSymbolInFile(symbolUse.Symbol) | ||
let lastIdent = List.last qualifiers | ||
return | ||
[| for symbolUse in symbolUses do | ||
yield { IsDefinition = symbolUse.IsFromDefinition | ||
TextSpan = CommonRoslynHelpers.FSharpRangeToTextSpan(sourceText, symbolUse.RangeAlternate) } |] | ||
|> fixInvalidSymbolSpans sourceText lastIdent | ||
| None -> return [||] | ||
| None -> return [||] | ||
} | ||
|
||
interface IDocumentHighlightsService with | ||
member __.GetDocumentHighlightsAsync(document, position, _documentsToSearch, cancellationToken) : Task<ImmutableArray<DocumentHighlights>> = | ||
async { | ||
match projectInfoManager.TryGetOptionsForEditingDocumentOrProject(document) 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! spans = FSharpDocumentHighlightsService.GetDocumentHighlights(checkerProvider.Checker, document.Id, sourceText, document.FilePath, position, defines, options, textVersion.GetHashCode(), cancellationToken) | ||
|
||
let highlightSpans = spans |> Array.map (fun span -> | ||
let kind = if span.IsDefinition then HighlightSpanKind.Definition else HighlightSpanKind.Reference | ||
HighlightSpan(span.TextSpan, kind)) | ||
|
||
return [| DocumentHighlights(document, highlightSpans.ToImmutableArray()) |].ToImmutableArray() | ||
| None -> return ImmutableArray<DocumentHighlights>() | ||
} | ||
|> CommonRoslynHelpers.StartAsyncAsTask(cancellationToken) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
101 changes: 101 additions & 0 deletions
101
vsintegration/tests/unittests/DocumentHighlightsServiceTests.fs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
|
||
// To run the tests in this file: | ||
// | ||
// Technique 1: Compile VisualFSharp.Unittests.dll and run it as a set of unit tests | ||
// | ||
// Technique 2: | ||
// | ||
// Enable some tests in the #if EXE section at the end of the file, | ||
// then compile this file as an EXE that has InternalsVisibleTo access into the | ||
// appropriate DLLs. This can be the quickest way to get turnaround on updating the tests | ||
// and capturing large amounts of structured output. | ||
(* | ||
cd Debug\net40\bin | ||
.\fsc.exe --define:EXE -r:.\Microsoft.Build.Utilities.Core.dll -o VisualFSharp.Unittests.exe -g --optimize- -r .\FSharp.LanguageService.Compiler.dll -r .\FSharp.Editor.dll -r nunit.framework.dll ..\..\..\tests\service\FsUnit.fs ..\..\..\tests\service\Common.fs /delaysign /keyfile:..\..\..\src\fsharp\msft.pubkey ..\..\..\vsintegration\tests\unittests\CompletionProviderTests.fs | ||
.\VisualFSharp.Unittests.exe | ||
*) | ||
// Technique 3: | ||
// | ||
// Use F# Interactive. This only works for FSharp.Compiler.Service.dll which has a public API | ||
|
||
// 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. | ||
module Microsoft.VisualStudio.FSharp.Editor.Tests.Roslyn.DocumentHighlightsServiceTests | ||
|
||
open System | ||
open System.Threading | ||
|
||
open NUnit.Framework | ||
|
||
open Microsoft.CodeAnalysis | ||
open Microsoft.CodeAnalysis.Text | ||
open Microsoft.VisualStudio.FSharp.Editor | ||
|
||
open Microsoft.FSharp.Compiler | ||
open Microsoft.FSharp.Compiler.SourceCodeServices | ||
|
||
let filePath = "C:\\test.fs" | ||
|
||
let internal options = { | ||
ProjectFileName = "C:\\test.fsproj" | ||
ProjectFileNames = [| filePath |] | ||
ReferencedProjects = [| |] | ||
OtherOptions = [| |] | ||
IsIncompleteTypeCheckEnvironment = true | ||
UseScriptResolutionRules = false | ||
LoadTime = DateTime.MaxValue | ||
UnresolvedReferences = None | ||
ExtraProjectInfo = None | ||
} | ||
|
||
let private getSpans (sourceText: SourceText) (caretPosition: int) = | ||
let documentId = DocumentId.CreateNewId(ProjectId.CreateNewId()) | ||
FSharpDocumentHighlightsService.GetDocumentHighlights( | ||
FSharpChecker.Instance, documentId, sourceText, filePath, caretPosition, [], options, 0, CancellationToken.None) | ||
|> Async.RunSynchronously | ||
|
||
let private span sourceText isDefinition (startLine, startCol) (endLine, endCol) = | ||
let range = Range.mkRange filePath (Range.mkPos startLine startCol) (Range.mkPos endLine endCol) | ||
{ IsDefinition = isDefinition | ||
TextSpan = CommonRoslynHelpers.FSharpRangeToTextSpan(sourceText, range) } | ||
|
||
[<Test>] | ||
let ShouldHighlightAllSimpleLocalSymbolReferences() = | ||
let fileContents = """ | ||
let foo x = | ||
x + x | ||
let y = foo 2 | ||
""" | ||
let sourceText = SourceText.From(fileContents) | ||
let caretPosition = fileContents.IndexOf("foo") + 1 | ||
let spans = getSpans sourceText caretPosition | ||
|
||
let expected = | ||
[| span sourceText true (2, 8) (2, 11) | ||
span sourceText false (4, 12) (4, 15) |] | ||
|
||
Assert.AreEqual(expected, spans) | ||
|
||
[<Test>] | ||
let ShouldHighlightAllQualifiedSymbolReferences() = | ||
let fileContents = """ | ||
let x = System.DateTime.Now | ||
let y = System.DateTime.MaxValue | ||
""" | ||
let sourceText = SourceText.From(fileContents) | ||
let caretPosition = fileContents.IndexOf("DateTime") + 1 | ||
let documentId = DocumentId.CreateNewId(ProjectId.CreateNewId()) | ||
|
||
let spans = getSpans sourceText caretPosition | ||
|
||
let expected = | ||
[| span sourceText false (2, 19) (2, 27) | ||
span sourceText false (3, 19) (3, 27) |] | ||
|
||
Assert.AreEqual(expected, spans) | ||
|
||
let caretPosition = fileContents.IndexOf("Now") + 1 | ||
let documentId = DocumentId.CreateNewId(ProjectId.CreateNewId()) | ||
let spans = getSpans sourceText caretPosition | ||
let expected = [| span sourceText false (2, 28) (2, 31) |] | ||
|
||
Assert.AreEqual(expected, spans) |
Oops, something went wrong.