From 216fc1e5e79c43c2cb32c5bcdeecd303cf6a35ea Mon Sep 17 00:00:00 2001 From: Robert Jeppesen Date: Wed, 14 Dec 2016 20:51:23 +0100 Subject: [PATCH] Implement F1 HelpContextService (#1966) * WIP: implement F1 HelpContextService Implements #1938 * Small cleanup * Enable context help tests for provided types * Delete old tests * Rebase against master --- .../src/FSharp.Editor/FSharp.Editor.fsproj | 1 + .../src/FSharp.Editor/HelpContextService.fs | 127 ++++++++++++++++++ ...1Keyword.fs => HelpContextServiceTests.fs} | 122 ++++++++++------- .../unittests/VisualFSharp.Unittests.fsproj | 4 +- 4 files changed, 201 insertions(+), 53 deletions(-) create mode 100644 vsintegration/src/FSharp.Editor/HelpContextService.fs rename vsintegration/tests/unittests/{Tests.LanguageService.F1Keyword.fs => HelpContextServiceTests.fs} (80%) diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index d2bab3f384f..1f8b896837b 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -46,6 +46,7 @@ + diff --git a/vsintegration/src/FSharp.Editor/HelpContextService.fs b/vsintegration/src/FSharp.Editor/HelpContextService.fs new file mode 100644 index 00000000000..eca3ee040c6 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/HelpContextService.fs @@ -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 + +[] +[, FSharpCommonConstants.FSharpLanguageName)>] +type internal FSharpHelpContextService + [] + ( + checkerProvider: FSharpCheckerProvider, + projectInfoManager: ProjectInfoManager + ) = + + static member GetHelpTerm(checker: FSharpChecker, sourceText : SourceText, fileName, options, span: TextSpan, tokens: List, 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<_> + diff --git a/vsintegration/tests/unittests/Tests.LanguageService.F1Keyword.fs b/vsintegration/tests/unittests/HelpContextServiceTests.fs similarity index 80% rename from vsintegration/tests/unittests/Tests.LanguageService.F1Keyword.fs rename to vsintegration/tests/unittests/HelpContextServiceTests.fs index a78ccc9ba86..6752fc0617d 100644 --- a/vsintegration/tests/unittests/Tests.LanguageService.F1Keyword.fs +++ b/vsintegration/tests/unittests/HelpContextServiceTests.fs @@ -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 -[] -type UsingMSBuild() = - inherit LanguageServiceBaseTests() +[] +type HelpContextServiceTests() = - member private this.TestF1Keywords(expectedKeywords, testLines, ?addtlRefAssy : list) = - 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) = + 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 - () - [] member public this.``NoKeyword.Negative`` () = let file = @@ -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) @@ -225,7 +250,6 @@ type UsingMSBuild() = this.TestF1Keywords(keywords, file, addtlRefAssy = [PathRelativeToTestAssembly(@"UnitTestsResources\MockTypeProviders\DummyProviderForLanguageServiceTesting.dll")]) - [] member public this.``EndOfLine``() = let file = @@ -369,9 +393,3 @@ type UsingMSBuild() = Some "System.Int32.ToString" ] this.TestF1Keywords(keywords, file) - - -// Context project system -[] -type UsingProjectSystem() = - inherit UsingMSBuild(VsOpts = LanguageServiceExtension.ProjectSystemTestFlavour) \ No newline at end of file diff --git a/vsintegration/tests/unittests/VisualFSharp.Unittests.fsproj b/vsintegration/tests/unittests/VisualFSharp.Unittests.fsproj index 066282814b0..feceea0d23b 100644 --- a/vsintegration/tests/unittests/VisualFSharp.Unittests.fsproj +++ b/vsintegration/tests/unittests/VisualFSharp.Unittests.fsproj @@ -41,7 +41,6 @@ - @@ -87,6 +86,9 @@ Roslyn\BraceMatchingServiceTests.fs + + Roslyn\HelpContextServiceTests.fs + Roslyn\IndentationServiceTests.fs