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