Skip to content

Commit d831740

Browse files
author
Omar Tawfik
committed
Added FSharpCompletionProviderTests
1 parent 02d473c commit d831740

File tree

3 files changed

+181
-40
lines changed

3 files changed

+181
-40
lines changed

vsintegration/src/FSharp.Editor/CompletionProvider.fs

Lines changed: 51 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,19 @@ open Microsoft.FSharp.Compiler.SourceCodeServices
3636
type internal FSharpCompletionProvider(workspace: Workspace, serviceProvider: SVsServiceProvider) =
3737
inherit CompletionProvider()
3838

39-
let completionTriggers = [ '.' ]
40-
let declarationItemsCache = ConditionalWeakTable<string, FSharpDeclarationListItem>()
39+
static let completionTriggers = [ '.' ]
40+
static let declarationItemsCache = ConditionalWeakTable<string, FSharpDeclarationListItem>()
4141

4242
let xmlMemberIndexService = serviceProvider.GetService(typeof<IVsXMLMemberIndexService>) :?> IVsXMLMemberIndexService
4343
let documentationBuilder = XmlDocumentation.CreateDocumentationBuilder(xmlMemberIndexService, serviceProvider.DTE)
4444

45-
override this.ShouldTriggerCompletion(sourceText: SourceText, caretPosition: int, trigger: CompletionTrigger, _: OptionSet) =
45+
static member ShouldTriggerCompletionAux(sourceText: SourceText, caretPosition: int, trigger: CompletionTriggerKind, filePath: string, defines: string list) =
4646
// Skip if we are at the start of a document
4747
if caretPosition = 0 then
4848
false
4949

5050
// Skip if it was triggered by an operation other than insertion
51-
else if not (trigger.Kind = CompletionTriggerKind.Insertion) then
51+
else if not (trigger = CompletionTriggerKind.Insertion) then
5252
false
5353

5454
// Skip if we are not on a completion trigger
@@ -57,51 +57,62 @@ type internal FSharpCompletionProvider(workspace: Workspace, serviceProvider: SV
5757

5858
// Trigger completion if we are on a valid classification type
5959
else
60-
let documentId = workspace.GetDocumentIdInCurrentContext(sourceText.Container)
61-
let document = workspace.CurrentSolution.GetDocument(documentId)
62-
63-
match FSharpLanguageService.GetOptions(document.Project.Id) with
64-
| Some(options) ->
65-
66-
let triggerPosition = caretPosition - 1
67-
let textLine = sourceText.Lines.GetLineFromPosition(triggerPosition)
68-
let defines = CompilerEnvironment.GetCompilationDefinesForEditing(document.Name, options.OtherOptions |> Seq.toList)
69-
let classifiedSpanOption =
70-
FSharpColorizationService.GetColorizationData(sourceText, textLine.Span, Some(document.FilePath), defines, CancellationToken.None)
71-
|> Seq.tryFind(fun classifiedSpan -> classifiedSpan.TextSpan.Contains(triggerPosition))
72-
73-
match classifiedSpanOption with
74-
| None -> false
75-
| Some(classifiedSpan) ->
76-
match classifiedSpan.ClassificationType with
77-
| ClassificationTypeNames.Comment -> false
78-
| ClassificationTypeNames.StringLiteral -> false
79-
| ClassificationTypeNames.ExcludedCode -> false
80-
| _ -> true // anything else is a valid classification type
60+
let triggerPosition = caretPosition - 1
61+
let textLine = sourceText.Lines.GetLineFromPosition(triggerPosition)
62+
let classifiedSpanOption =
63+
FSharpColorizationService.GetColorizationData(sourceText, textLine.Span, Some(filePath), defines, CancellationToken.None)
64+
|> Seq.tryFind(fun classifiedSpan -> classifiedSpan.TextSpan.Contains(triggerPosition))
8165

66+
match classifiedSpanOption with
8267
| None -> false
68+
| Some(classifiedSpan) ->
69+
match classifiedSpan.ClassificationType with
70+
| ClassificationTypeNames.Comment -> false
71+
| ClassificationTypeNames.StringLiteral -> false
72+
| ClassificationTypeNames.ExcludedCode -> false
73+
| _ -> true // anything else is a valid classification type
74+
75+
static member ProvideCompletionsAsyncAux(sourceText: SourceText, caretPosition: int, options: FSharpProjectOptions, filePath: string, textVersionHash: int) = async {
76+
let! parseResults = FSharpChecker.Instance.ParseFileInProject(filePath, sourceText.ToString(), options)
77+
let! checkFileAnswer = FSharpChecker.Instance.CheckFileInProject(parseResults, filePath, textVersionHash, sourceText.ToString(), options)
78+
let checkFileResults = match checkFileAnswer with
79+
| FSharpCheckFileAnswer.Aborted -> failwith "Compilation isn't complete yet"
80+
| FSharpCheckFileAnswer.Succeeded(results) -> results
81+
82+
let textLine = sourceText.Lines.GetLineFromPosition(caretPosition)
83+
let textLineNumber = textLine.LineNumber + 1 // Roslyn line numbers are zero-based
84+
let qualifyingNames, partialName = QuickParse.GetPartialLongNameEx(textLine.ToString(), caretPosition - textLine.Start - 1)
85+
let! declarations = checkFileResults.GetDeclarationListInfo(Some(parseResults), textLineNumber, caretPosition, textLine.ToString(), qualifyingNames, partialName)
86+
87+
let results = List<CompletionItem>()
88+
89+
for declarationItem in declarations.Items do
90+
let completionItem = CompletionItem.Create(declarationItem.Name)
91+
declarationItemsCache.Add(completionItem.DisplayText, declarationItem)
92+
results.Add(completionItem)
93+
94+
return results
95+
}
96+
97+
98+
override this.ShouldTriggerCompletion(sourceText: SourceText, caretPosition: int, trigger: CompletionTrigger, _: OptionSet) =
99+
let documentId = workspace.GetDocumentIdInCurrentContext(sourceText.Container)
100+
let document = workspace.CurrentSolution.GetDocument(documentId)
101+
102+
match FSharpLanguageService.GetOptions(document.Project.Id) with
103+
| None -> false
104+
| Some(options) ->
105+
let defines = CompilerEnvironment.GetCompilationDefinesForEditing(document.Name, options.OtherOptions |> Seq.toList)
106+
FSharpCompletionProvider.ShouldTriggerCompletionAux(sourceText, caretPosition, trigger.Kind, document.FilePath, defines)
83107

84108
override this.ProvideCompletionsAsync(context: Microsoft.CodeAnalysis.Completion.CompletionContext) =
85109
let computation = async {
86110
match FSharpLanguageService.GetOptions(context.Document.Project.Id) with
87111
| Some(options) ->
88112
let! sourceText = context.Document.GetTextAsync(context.CancellationToken) |> Async.AwaitTask
89-
let! parseResults = FSharpChecker.Instance.ParseFileInProject(context.Document.FilePath, sourceText.ToString(), options)
90113
let! textVersion = context.Document.GetTextVersionAsync(context.CancellationToken) |> Async.AwaitTask
91-
let! checkFileAnswer = FSharpChecker.Instance.CheckFileInProject(parseResults, context.Document.FilePath, textVersion.GetHashCode(), sourceText.ToString(), options)
92-
let checkFileResults = match checkFileAnswer with
93-
| FSharpCheckFileAnswer.Aborted -> failwith "Compilation isn't complete yet"
94-
| FSharpCheckFileAnswer.Succeeded(results) -> results
95-
96-
let textLine = sourceText.Lines.GetLineFromPosition(context.Position)
97-
let textLineNumber = textLine.LineNumber + 1 // Roslyn line numbers are zero-based
98-
let qualifyingNames, partialName = QuickParse.GetPartialLongNameEx(textLine.ToString(), context.Position - textLine.Start - 1)
99-
let! declarations = checkFileResults.GetDeclarationListInfo(Some(parseResults), textLineNumber, context.Position, textLine.ToString(), qualifyingNames, partialName)
100-
101-
for declarationItem in declarations.Items do
102-
let completionItem = CompletionItem.Create(declarationItem.Name)
103-
declarationItemsCache.Add(completionItem.DisplayText, declarationItem)
104-
context.AddItem(completionItem)
114+
let! results = FSharpCompletionProvider.ProvideCompletionsAsyncAux(sourceText, context.Position, options, context.Document.FilePath, textVersion.GetHashCode())
115+
context.AddItems(results)
105116
| None -> ()
106117
}
107118

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// 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.
2+
namespace Microsoft.VisualStudio.FSharp.Editor.Tests.Roslyn
3+
4+
open System
5+
open System.Threading
6+
open System.Linq
7+
8+
open NUnit.Framework
9+
10+
open Microsoft.CodeAnalysis.Completion
11+
open Microsoft.CodeAnalysis.Classification
12+
open Microsoft.CodeAnalysis.Text
13+
open Microsoft.VisualStudio.FSharp.Editor
14+
15+
open Microsoft.VisualStudio.FSharp.Editor
16+
open Microsoft.VisualStudio.FSharp.LanguageService
17+
18+
open Microsoft.FSharp.Compiler.SourceCodeServices
19+
open Microsoft.FSharp.Compiler.Range
20+
21+
[<TestFixture>]
22+
type CompletionProviderTests() =
23+
let filePath = "C:\\test.fs"
24+
let options: FSharpProjectOptions = {
25+
ProjectFileName = "C:\\test.fsproj"
26+
ProjectFileNames = [| filePath |]
27+
ReferencedProjects = [| |]
28+
OtherOptions = [| |]
29+
IsIncompleteTypeCheckEnvironment = true
30+
UseScriptResolutionRules = false
31+
LoadTime = DateTime.MaxValue
32+
UnresolvedReferences = None
33+
}
34+
35+
member private this.VerifyCompletionList(fileContents: string, marker: string, expected: string list, unexpected: string list) =
36+
let caretPosition = fileContents.IndexOf(marker) + marker.Length
37+
let results = FSharpCompletionProvider.ProvideCompletionsAsyncAux(SourceText.From(fileContents), caretPosition, options, filePath, 0) |>
38+
Async.RunSynchronously |>
39+
Seq.map(fun result -> result.DisplayText)
40+
41+
for item in expected do
42+
Assert.IsTrue(results.Contains(item), "Completions should contain '{0}'. Got '{1}'.", item, String.Join(", ", results))
43+
44+
for item in unexpected do
45+
Assert.IsFalse(results.Contains(item), "Completions should not contain '{0}'. Got '{1}'", item, String.Join(", ", results))
46+
47+
[<TestCase("x", false)>]
48+
[<TestCase("y", false)>]
49+
[<TestCase("1", false)>]
50+
[<TestCase("2", false)>]
51+
[<TestCase("x +", false)>]
52+
[<TestCase("Console.Write", false)>]
53+
[<TestCase("System.", true)>]
54+
[<TestCase("Console.", true)>]
55+
member this.ShouldTriggerCompletionAtCorrectMarkers(marker: string, shouldBeTriggered: bool) =
56+
let fileContents = """
57+
let x = 1
58+
let y = 2
59+
System.Console.WriteLine(x + y)
60+
"""
61+
62+
let caretPosition = fileContents.IndexOf(marker) + marker.Length
63+
let triggered = FSharpCompletionProvider.ShouldTriggerCompletionAux(SourceText.From(fileContents), caretPosition, CompletionTriggerKind.Insertion, filePath, [])
64+
Assert.AreEqual(shouldBeTriggered, triggered, "FSharpCompletionProvider.ShouldTriggerCompletionAux() should compute the correct result")
65+
66+
[<TestCase(CompletionTriggerKind.Deletion)>]
67+
[<TestCase(CompletionTriggerKind.Other)>]
68+
[<TestCase(CompletionTriggerKind.Snippets)>]
69+
member this.ShouldNotTriggerCompletionAfterAnyTriggerOtherThanInsertion(triggerKind: CompletionTriggerKind) =
70+
let fileContents = "System.Console.WriteLine(123)"
71+
let caretPosition = fileContents.IndexOf("System.")
72+
let triggered = FSharpCompletionProvider.ShouldTriggerCompletionAux(SourceText.From(fileContents), caretPosition, triggerKind, filePath, [])
73+
Assert.IsFalse(triggered, "FSharpCompletionProvider.ShouldTriggerCompletionAux() should not trigger")
74+
75+
[<Test>]
76+
member this.ShouldNotTriggerCompletionInStringLiterals() =
77+
let fileContents = "let literal = \"System.Console.WriteLine()\""
78+
let caretPosition = fileContents.IndexOf("System.")
79+
let triggered = FSharpCompletionProvider.ShouldTriggerCompletionAux(SourceText.From(fileContents), caretPosition, CompletionTriggerKind.Insertion, filePath, [])
80+
Assert.IsFalse(triggered, "FSharpCompletionProvider.ShouldTriggerCompletionAux() should not trigger")
81+
82+
[<Test>]
83+
member this.ShouldNotTriggerCompletionInComments() =
84+
let fileContents = """
85+
(*
86+
This is a comment
87+
System.Console.WriteLine()
88+
*)
89+
"""
90+
let caretPosition = fileContents.IndexOf("System.")
91+
let triggered = FSharpCompletionProvider.ShouldTriggerCompletionAux(SourceText.From(fileContents), caretPosition, CompletionTriggerKind.Insertion, filePath, [])
92+
Assert.IsFalse(triggered, "FSharpCompletionProvider.ShouldTriggerCompletionAux() should not trigger")
93+
94+
[<Test>]
95+
member this.ShouldNotTriggerCompletionInExcludedCode() =
96+
let fileContents = """
97+
#if UNDEFINED
98+
System.Console.WriteLine()
99+
#endif
100+
"""
101+
let caretPosition = fileContents.IndexOf("System.")
102+
let triggered = FSharpCompletionProvider.ShouldTriggerCompletionAux(SourceText.From(fileContents), caretPosition, CompletionTriggerKind.Insertion, filePath, [])
103+
Assert.IsFalse(triggered, "FSharpCompletionProvider.ShouldTriggerCompletionAux() should not trigger")
104+
105+
[<Test>]
106+
member this.ShouldDisplayTypeMembers() =
107+
let fileContents = """
108+
type T1() =
109+
member this.M1 = 5
110+
member this.M2 = "literal"
111+
112+
[<EntryPoint>]
113+
let main argv =
114+
let obj = T1()
115+
obj.
116+
"""
117+
this.VerifyCompletionList(fileContents, "obj.", ["M1"; "M2"], ["System"])
118+
119+
[<Test>]
120+
member this.ShouldDisplaySystemNamespace() =
121+
let fileContents = """
122+
type T1 =
123+
member this.M1 = 5
124+
member this.M2 = "literal"
125+
System.Console.WriteLine()
126+
"""
127+
this.VerifyCompletionList(fileContents, "System.", ["Console"; "Array"; "String"], ["T1"; "M1"; "M2"])

vsintegration/tests/unittests/VisualFSharp.Unittests.fsproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@
5252
<Compile Include="DocumentDiagnosticAnalyzerTests.fs">
5353
<Link>Roslyn\Diagnostics\DocumentDiagnosticAnalyzerTests.fs</Link>
5454
</Compile>
55+
<Compile Include="CompletionProviderTests.fs">
56+
<Link>Roslyn\Completion\CompletionProviderTests.fs</Link>
57+
</Compile>
5558
<Compile Include="Tests.InternalCollections.fs" />
5659
<Compile Include="Tests.Build.fs" />
5760
<Compile Include="Tests.TaskReporter.fs" />

0 commit comments

Comments
 (0)