Skip to content

Commit

Permalink
Implement F1 HelpContextService (#1966)
Browse files Browse the repository at this point in the history
* WIP: implement F1 HelpContextService

Implements #1938

* Small cleanup

* Enable context help tests for provided types

* Delete old tests

* Rebase against master
  • Loading branch information
rojepp authored and KevinRansom committed Dec 14, 2016
1 parent 526a409 commit 216fc1e
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 53 deletions.
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="BlockComment\CommentUncommentService.fs"/>
<Compile Include="QuickInfo\QuickInfoProvider.fs"/>
<Compile Include="NavigationBarItemService.fs" />
<Compile Include="HelpContextService.fs"/>
<Compile Include="ContentType.fs" />
<Compile Include="FsiCommandService.fs" />
</ItemGroup>
Expand Down
127 changes: 127 additions & 0 deletions vsintegration/src/FSharp.Editor/HelpContextService.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// 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.Threading.Tasks
open System.Collections.Generic
open System.Composition
open Microsoft.CodeAnalysis.Editor
open Microsoft.CodeAnalysis.Text
open Microsoft.CodeAnalysis.Classification
open Microsoft.VisualStudio.FSharp.LanguageService
open Microsoft.FSharp.Compiler.SourceCodeServices
open Microsoft.VisualStudio.LanguageServices.Implementation.F1Help
open Microsoft.CodeAnalysis.Host.Mef
open Microsoft.FSharp.Compiler.Range

[<Shared>]
[<ExportLanguageService(typeof<IHelpContextService>, FSharpCommonConstants.FSharpLanguageName)>]
type internal FSharpHelpContextService
[<ImportingConstructor>]
(
checkerProvider: FSharpCheckerProvider,
projectInfoManager: ProjectInfoManager
) =

static member GetHelpTerm(checker: FSharpChecker, sourceText : SourceText, fileName, options, span: TextSpan, tokens: List<ClassifiedSpan>, textVersion) = async {
let! _parse,check = checker.ParseAndCheckFileInProject(fileName, textVersion, sourceText.ToString(), options)
match check with
| FSharpCheckFileAnswer.Aborted -> return None
| FSharpCheckFileAnswer.Succeeded(check) ->
let textLines = sourceText.Lines
let lineInfo = textLines.GetLineFromPosition(span.Start)
let line = lineInfo.LineNumber
let lineText = lineInfo.ToString()

let caretColumn = textLines.GetLinePosition(span.Start).Character

let shouldTryToFindSurroundingIdent (token : ClassifiedSpan) =
let span = token.TextSpan
let content = sourceText.ToString().Substring(span.Start, span.End - span.Start)
match token.ClassificationType with
| ClassificationTypeNames.Text
| ClassificationTypeNames.WhiteSpace -> true
| ClassificationTypeNames.Operator when
content = "." -> true
| _ -> false

let keyword =
let tokenInformation, col =
let col =
if caretColumn = lineText.Length && caretColumn > 0 then
// if we are at the end of the line, we always step back one character
caretColumn - 1
else
caretColumn

let getTokenAt line col =
if col < 0 || line < 0 then None else
let start = textLines.[line].Start + col
let span = TextSpan.FromBounds(start, start + 1)
tokens
|> Seq.tryFindIndex(fun t -> t.TextSpan.Contains(span))
|> Option.map (fun i -> tokens.[i])

match getTokenAt line col with
| Some t as original -> // when col > 0 && shouldTryToFindSurroundingIdent t ->
if shouldTryToFindSurroundingIdent t then
match getTokenAt line (col - 1) with
| Some t as newInfo when not (shouldTryToFindSurroundingIdent t) -> newInfo, col - 1
| _ ->
match getTokenAt line (col + 1) with
| Some t as newInfo when not (shouldTryToFindSurroundingIdent t) -> newInfo, col + 1
| _ -> original, col
else original, col
| otherwise -> otherwise, col

match tokenInformation with
| None -> None
| Some token ->
match token.ClassificationType with
| ClassificationTypeNames.Keyword
| ClassificationTypeNames.Operator
| ClassificationTypeNames.PreprocessorKeyword ->
sourceText.GetSubText(token.TextSpan).ToString() + "_FS" |> Some
| ClassificationTypeNames.Comment -> Some "comment_FS"
| ClassificationTypeNames.Identifier ->
try
let possibleIdentifier = QuickParse.GetCompleteIdentifierIsland false lineText col
match possibleIdentifier with
| None -> None
| Some(s,colAtEndOfNames, _) ->
if check.HasFullTypeCheckInfo then
let qualId = PrettyNaming.GetLongNameFromString s
check.GetF1KeywordAlternate(Line.fromZ line, colAtEndOfNames, lineText, qualId)
|> Async.RunSynchronously
else None
with e ->
Assert.Exception (e)
reraise()
| _ -> None
return keyword
}

interface IHelpContextService with
member this.Language with get () = "fsharp"
member this.Product with get () = "fsharp"

member this.GetHelpTermAsync(document, textSpan, cancellationToken) =
async {
match projectInfoManager.TryGetOptionsForEditingDocumentOrProject(document) with
| Some options ->
let! sourceText = document.GetTextAsync(cancellationToken) |> Async.AwaitTask
let! textVersion = document.GetTextVersionAsync(cancellationToken) |> Async.AwaitTask
let defines = projectInfoManager.GetCompilationDefinesForEditingDocument(document)
let textLine = sourceText.Lines.GetLineFromPosition(textSpan.Start)
let tokens = CommonHelpers.getColorizationData(document.Id, sourceText, textLine.Span, Some document.Name, defines, cancellationToken)
let! keyword = FSharpHelpContextService.GetHelpTerm(checkerProvider.Checker, sourceText, document.FilePath, options, textSpan, tokens, textVersion.GetHashCode())

return match keyword with
| Some k -> k
| None -> ""
| None -> return ""
} |> CommonRoslynHelpers.StartAsyncAsTask cancellationToken

member this.FormatSymbol(_symbol) = Unchecked.defaultof<_>

Original file line number Diff line number Diff line change
@@ -1,50 +1,75 @@
// 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 Tests.LanguageService.F1Keyword
namespace Microsoft.VisualStudio.FSharp.Editor.Tests.Roslyn

open System
open System.Threading

open NUnit.Framework
open Salsa.Salsa
open Salsa.VsOpsUtils
open UnitTests.TestLib.Salsa

open Microsoft.CodeAnalysis.Classification
open Microsoft.CodeAnalysis.Editor
open Microsoft.CodeAnalysis.Text
open Microsoft.CodeAnalysis
open Microsoft.FSharp.Compiler.SourceCodeServices
open Microsoft.VisualStudio.FSharp.Editor
open Microsoft.VisualStudio.FSharp.LanguageService
open UnitTests.TestLib.Utils
open UnitTests.TestLib.LanguageService
open UnitTests.TestLib.ProjectSystem

[<TestFixture>]
type UsingMSBuild() =
inherit LanguageServiceBaseTests()
[<TestFixture>]
type HelpContextServiceTests() =

member private this.TestF1Keywords(expectedKeywords, testLines, ?addtlRefAssy : list<string>) =
let lines, poslist = // poslist is in _cursor_ postions. Cursor postions are 1-based.
let rec extractPos (col:int) row (s:string) poslist =
let next = s.IndexOf("$", col)
if next < 0 then
s, poslist
else
let s = s.Remove(next, 1)
extractPos next row s ((row,next+1)::poslist)
let _, l, p =
List.fold (fun (row,lines,poslist) s ->
let (s', poslist') = extractPos 0 row s poslist
(row+1, (s'::lines),poslist')) (1,[],[]) testLines
List.rev l , List.rev p
let fileName = "C:\\test.fs"
let options: FSharpProjectOptions = {
ProjectFileName = "C:\\test.fsproj"
ProjectFileNames = [| fileName |]
ReferencedProjects = [| |]
OtherOptions = [| |]
IsIncompleteTypeCheckEnvironment = true
UseScriptResolutionRules = false
LoadTime = DateTime.MaxValue
UnresolvedReferences = None
ExtraProjectInfo = None
}

let markers (source:string) =
let mutable cnt = 0
[
for i in 0 .. (source.Length - 1) do
if source.[i] = '$' then
yield (i - cnt)
cnt <- cnt + 1
]

member private this.TestF1Keywords(expectedKeywords: string option list, lines : string list, ?addtlRefAssy : list<string>) =
let newOptions =
let refs =
defaultArg addtlRefAssy []
|> List.map (fun r -> "-r:" + r)
|> Array.ofList
{ options with OtherOptions = Array.append options.OtherOptions refs }

let fileContents = String.Join("\r\n", lines)
let version = fileContents.GetHashCode()
let sourceText = SourceText.From(fileContents.Replace("$", ""))

let res = [
for marker in markers fileContents do
let span = TextSpan(marker, 0)
let textLine = sourceText.Lines.GetLineFromPosition(marker)
let documentId = DocumentId.CreateNewId(ProjectId.CreateNewId())
let tokens = CommonHelpers.getColorizationData(documentId, sourceText, textLine.Span, Some "test.fs", [], CancellationToken.None)

yield FSharpHelpContextService.GetHelpTerm(FSharpChecker.Instance, sourceText, fileName, newOptions, span, tokens, version)
|> Async.RunSynchronously
]
let equalLength = List.length expectedKeywords = List.length res
Assert.True(equalLength)

List.iter2(fun exp res ->
Assert.AreEqual(exp, res)
) expectedKeywords res

let refs =
let standard = ["mscorlib"; "System"; "System.Core"]
match addtlRefAssy with
| Some r -> standard @ r
| _ -> standard

let (_,_, file) = this.CreateSingleFileProject(lines, references = refs)
Assert.IsTrue(List.length expectedKeywords = List.length poslist, sprintf "number of keywords (%d) does not match positions (%d)" (List.length expectedKeywords) (List.length poslist))
List.iter2
(fun expectedKeyword (row,col) ->
MoveCursorTo(file,row,col)
let keyword = GetF1KeywordAtCursor file
Assert.AreEqual(expectedKeyword, keyword)) expectedKeywords poslist
()

[<Test>]
member public this.``NoKeyword.Negative`` () =
let file =
Expand Down Expand Up @@ -156,13 +181,13 @@ type UsingMSBuild() =
]
let keywords =
[
Some "File1.escaped func"
Some "File1.escaped value"
Some "File1.x"
Some "File1.escaped func"
Some "File1.escaped value"
Some "File1.z"
Some "File1.z"
Some "Test.escaped func"
Some "Test.escaped value"
Some "Test.x"
Some "Test.escaped func"
Some "Test.escaped value"
Some "Test.z"
Some "Test.z"
]
this.TestF1Keywords(keywords, file)

Expand Down Expand Up @@ -225,7 +250,6 @@ type UsingMSBuild() =
this.TestF1Keywords(keywords, file,
addtlRefAssy = [PathRelativeToTestAssembly(@"UnitTestsResources\MockTypeProviders\DummyProviderForLanguageServiceTesting.dll")])


[<Test>]
member public this.``EndOfLine``() =
let file =
Expand Down Expand Up @@ -369,9 +393,3 @@ type UsingMSBuild() =
Some "System.Int32.ToString"
]
this.TestF1Keywords(keywords, file)


// Context project system
[<TestFixture>]
type UsingProjectSystem() =
inherit UsingMSBuild(VsOpts = LanguageServiceExtension.ProjectSystemTestFlavour)
4 changes: 3 additions & 1 deletion vsintegration/tests/unittests/VisualFSharp.Unittests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
<Compile Include="Tests.XmlDocComments.fs" />
<Compile Include="Tests.LanguageService.General.fs" />
<Compile Include="Tests.LanguageService.Completion.fs" />
<Compile Include="Tests.LanguageService.F1Keyword.fs" />
<Compile Include="Tests.LanguageService.IncrementalBuild.fs" />
<Compile Include="Tests.LanguageService.GotoDefinition.fs" />
<Compile Include="Tests.LanguageService.NavigationBar.fs" />
Expand Down Expand Up @@ -87,6 +86,9 @@
<Compile Include="BraceMatchingServiceTests.fs">
<Link>Roslyn\BraceMatchingServiceTests.fs</Link>
</Compile>
<Compile Include="HelpContextServiceTests.fs">
<Link>Roslyn\HelpContextServiceTests.fs</Link>
</Compile>
<Compile Include="IndentationServiceTests.fs">
<Link>Roslyn\IndentationServiceTests.fs</Link>
</Compile>
Expand Down

0 comments on commit 216fc1e

Please sign in to comment.