/
CompletionProvider.fs
303 lines (249 loc) · 17.4 KB
/
CompletionProvider.fs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
namespace Microsoft.VisualStudio.FSharp.Editor
open System
open System.Collections.Generic
open System.Collections.Immutable
open System.Threading
open System.Threading.Tasks
open Microsoft.CodeAnalysis
open Microsoft.CodeAnalysis.Completion
open Microsoft.CodeAnalysis.Options
open Microsoft.CodeAnalysis.Text
open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Completion
open Microsoft.VisualStudio.Shell
open FSharp.Compiler
open FSharp.Compiler.Range
open FSharp.Compiler.SourceCodeServices
module Logger = Microsoft.VisualStudio.FSharp.Editor.Logger
type internal FSharpCompletionProvider
(
workspace: Workspace,
serviceProvider: SVsServiceProvider,
checkerProvider: FSharpCheckerProvider,
projectInfoManager: FSharpProjectOptionsManager,
assemblyContentProvider: AssemblyContentProvider
) =
inherit CompletionProvider()
static let userOpName = "CompletionProvider"
// Save the backing data in a cache, we need to save for at least the length of the completion session
// See https://github.com/Microsoft/visualfsharp/issues/4714
static let mutable declarationItems: FSharpDeclarationListItem[] = [||]
static let [<Literal>] NameInCodePropName = "NameInCode"
static let [<Literal>] FullNamePropName = "FullName"
static let [<Literal>] IsExtensionMemberPropName = "IsExtensionMember"
static let [<Literal>] NamespaceToOpenPropName = "NamespaceToOpen"
static let [<Literal>] IsKeywordPropName = "IsKeyword"
static let [<Literal>] IndexPropName = "Index"
static let keywordCompletionItems =
Keywords.KeywordsWithDescription
|> List.filter (fun (keyword, _) -> not (PrettyNaming.IsOperatorName keyword))
|> List.sortBy (fun (keyword, _) -> keyword)
|> List.mapi (fun n (keyword, description) ->
FSharpCommonCompletionItem.Create(keyword, null, CompletionItemRules.Default, Nullable Glyph.Keyword, sortText = sprintf "%06d" (1000000 + n))
.AddProperty("description", description)
.AddProperty(IsKeywordPropName, ""))
let checker = checkerProvider.Checker
let settings: EditorOptions = workspace.Services.GetService()
let documentationBuilder = XmlDocumentation.CreateDocumentationBuilder(serviceProvider.XMLMemberIndexService)
static let noCommitOnSpaceRules =
// These are important. They make sure we don't _commit_ autocompletion when people don't expect them to. Some examples:
//
// * type Foo() =
// member val a = 12 with get, <<---- Don't commit autocomplete!
//
// * type MyRecord = { name: <<---- Don't commit autocomplete!
//
// * type My< <<---- Don't commit autocomplete!
//
// * let myClassInstance = MyClass(Date= <<---- Don't commit autocomplete!
//
// * let xs = [1..10] <<---- Don't commit autocomplete! (same for arrays)
let noCommitChars = [|' '; '='; ','; '.'; '<'; '>'; '('; ')'; '!'; ':'; '['; ']'; '|'|].ToImmutableArray()
CompletionItemRules.Default.WithCommitCharacterRules(ImmutableArray.Create (CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, noCommitChars)))
static let getRules showAfterCharIsTyped = if showAfterCharIsTyped then noCommitOnSpaceRules else CompletionItemRules.Default
static let mruItems = Dictionary<(* Item.FullName *) string, (* hints *) int>()
static member ShouldTriggerCompletionAux(sourceText: SourceText, caretPosition: int, trigger: CompletionTriggerKind, getInfo: (unit -> DocumentId * string * string list), intelliSenseOptions: IntelliSenseOptions) =
if caretPosition = 0 then
false
else
let triggerPosition = caretPosition - 1
let triggerChar = sourceText.[triggerPosition]
if trigger = CompletionTriggerKind.Deletion && intelliSenseOptions.ShowAfterCharIsDeleted then
Char.IsLetterOrDigit(sourceText.[triggerPosition]) || triggerChar = '.'
elif not (trigger = CompletionTriggerKind.Insertion) then
false
else
// Do not trigger completion if it's not single dot, i.e. range expression
if not intelliSenseOptions.ShowAfterCharIsTyped && triggerPosition > 0 && sourceText.[triggerPosition - 1] = '.' then
false
else
let documentId, filePath, defines = getInfo()
CompletionUtils.shouldProvideCompletion(documentId, filePath, defines, sourceText, triggerPosition) &&
(triggerChar = '.' || (intelliSenseOptions.ShowAfterCharIsTyped && CompletionUtils.isStartingNewWord(sourceText, triggerPosition)))
static member ProvideCompletionsAsyncAux(checker: FSharpChecker, sourceText: SourceText, caretPosition: int, options: FSharpProjectOptions, filePath: string,
textVersionHash: int, getAllSymbols: FSharpCheckFileResults -> AssemblySymbol list, languageServicePerformanceOptions: LanguageServicePerformanceOptions, intellisenseOptions: IntelliSenseOptions) =
asyncMaybe {
let! parseResults, _, checkFileResults = checker.ParseAndCheckDocument(filePath, textVersionHash, sourceText, options, languageServicePerformanceOptions, userOpName = userOpName)
let textLines = sourceText.Lines
let caretLinePos = textLines.GetLinePosition(caretPosition)
let caretLine = textLines.GetLineFromPosition(caretPosition)
let fcsCaretLineNumber = Line.fromZ caretLinePos.Line // Roslyn line numbers are zero-based, FSharp.Compiler.Service line numbers are 1-based
let caretLineColumn = caretLinePos.Character
let partialName = QuickParse.GetPartialLongNameEx(caretLine.ToString(), caretLineColumn - 1)
let getAllSymbols() =
getAllSymbols checkFileResults
|> List.filter (fun assemblySymbol ->
assemblySymbol.FullName.Contains "." && not (PrettyNaming.IsOperatorName assemblySymbol.Symbol.DisplayName))
let! declarations = checkFileResults.GetDeclarationListInfo(Some(parseResults), fcsCaretLineNumber, caretLine.ToString(),
partialName, getAllSymbols, userOpName=userOpName) |> liftAsync
let results = List<Completion.CompletionItem>()
declarationItems <-
declarations.Items
|> Array.sortWith (fun x y ->
let mutable n = (not x.IsResolved).CompareTo(not y.IsResolved)
if n <> 0 then n else
n <- (CompletionUtils.getKindPriority x.Kind).CompareTo(CompletionUtils.getKindPriority y.Kind)
if n <> 0 then n else
n <- (not x.IsOwnMember).CompareTo(not y.IsOwnMember)
if n <> 0 then n else
n <- String.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase)
if n <> 0 then n else
x.MinorPriority.CompareTo(y.MinorPriority))
let maxHints = if mruItems.Values.Count = 0 then 0 else Seq.max mruItems.Values
declarationItems |> Array.iteri (fun number declarationItem ->
let glyph = Tokenizer.FSharpGlyphToRoslynGlyph (declarationItem.Glyph, declarationItem.Accessibility)
let name =
match declarationItem.NamespaceToOpen with
| Some namespaceToOpen -> sprintf "%s (open %s)" declarationItem.Name namespaceToOpen
| _ -> declarationItem.Name
let filterText =
match declarationItem.NamespaceToOpen, declarationItem.Name.Split '.' with
// There is no namespace to open and the item name does not contain dots, so we don't need to pass special FilterText to Roslyn.
| None, [|_|] -> null
// Either we have a namespace to open ("DateTime (open System)") or item name contains dots ("Array.map"), or both.
// We are passing last part of long ident as FilterText.
| _, idents -> Array.last idents
let completionItem =
FSharpCommonCompletionItem.Create(name, null, rules = getRules intellisenseOptions.ShowAfterCharIsTyped, glyph = Nullable glyph, filterText = filterText)
.AddProperty(FullNamePropName, declarationItem.FullName)
let completionItem =
match declarationItem.Kind with
| CompletionItemKind.Method (isExtension = true) ->
completionItem.AddProperty(IsExtensionMemberPropName, "")
| _ -> completionItem
let completionItem =
if name <> declarationItem.NameInCode then
completionItem.AddProperty(NameInCodePropName, declarationItem.NameInCode)
else completionItem
let completionItem =
match declarationItem.NamespaceToOpen with
| Some ns -> completionItem.AddProperty(NamespaceToOpenPropName, ns)
| None -> completionItem
let completionItem = completionItem.AddProperty(IndexPropName, string number)
let priority =
match mruItems.TryGetValue declarationItem.FullName with
| true, hints -> maxHints - hints
| _ -> number + maxHints + 1
let sortText = priority.ToString("D6")
let completionItem = completionItem.WithSortText(sortText)
results.Add(completionItem))
if results.Count > 0 && not declarations.IsForType && not declarations.IsError && List.isEmpty partialName.QualifyingIdents then
let lineStr = textLines.[caretLinePos.Line].ToString()
let completionContext =
parseResults.ParseTree
|> Option.bind (fun parseTree ->
UntypedParseImpl.TryGetCompletionContext(Pos.fromZ caretLinePos.Line caretLinePos.Character, parseTree, lineStr))
match completionContext with
| None -> results.AddRange(keywordCompletionItems)
| _ -> ()
return results
}
override this.ShouldTriggerCompletion(sourceText: SourceText, caretPosition: int, trigger: CompletionTrigger, _: OptionSet) =
use _logBlock = Logger.LogBlock LogEditorFunctionId.Completion_ShouldTrigger
let getInfo() =
let documentId = workspace.GetDocumentIdInCurrentContext(sourceText.Container)
let document = workspace.CurrentSolution.GetDocument(documentId)
let defines = projectInfoManager.GetCompilationDefinesForEditingDocument(document)
(documentId, document.FilePath, defines)
FSharpCompletionProvider.ShouldTriggerCompletionAux(sourceText, caretPosition, trigger.Kind, getInfo, settings.IntelliSense)
override this.ProvideCompletionsAsync(context: Completion.CompletionContext) =
asyncMaybe {
use _logBlock = Logger.LogBlockMessage context.Document.Name LogEditorFunctionId.Completion_ProvideCompletionsAsync
let document = context.Document
let! sourceText = context.Document.GetTextAsync(context.CancellationToken)
let defines = projectInfoManager.GetCompilationDefinesForEditingDocument(document)
do! Option.guard (CompletionUtils.shouldProvideCompletion(document.Id, document.FilePath, defines, sourceText, context.Position))
let! _parsingOptions, projectOptions = projectInfoManager.TryGetOptionsForEditingDocumentOrProject(document, context.CancellationToken)
let! textVersion = context.Document.GetTextVersionAsync(context.CancellationToken)
let getAllSymbols(fileCheckResults: FSharpCheckFileResults) =
if settings.IntelliSense.IncludeSymbolsFromUnopenedNamespacesOrModules
then assemblyContentProvider.GetAllEntitiesInProjectAndReferencedAssemblies(fileCheckResults)
else []
let! results =
FSharpCompletionProvider.ProvideCompletionsAsyncAux(checker, sourceText, context.Position, projectOptions, document.FilePath,
textVersion.GetHashCode(), getAllSymbols, settings.LanguageServicePerformance, settings.IntelliSense)
context.AddItems(results)
} |> Async.Ignore |> RoslynHelpers.StartAsyncUnitAsTask context.CancellationToken
override this.GetDescriptionAsync(document: Document, completionItem: Completion.CompletionItem, cancellationToken: CancellationToken): Task<CompletionDescription> =
async {
use _logBlock = Logger.LogBlockMessage document.Name LogEditorFunctionId.Completion_GetDescriptionAsync
match completionItem.Properties.TryGetValue IndexPropName with
| true, completionItemIndexStr ->
let completionItemIndex = int completionItemIndexStr
if completionItemIndex < declarationItems.Length then
let declarationItem = declarationItems.[completionItemIndex]
let! description = declarationItem.StructuredDescriptionTextAsync
let documentation = List()
let collector = RoslynHelpers.CollectTaggedText documentation
// mix main description and xmldoc by using one collector
XmlDocumentation.BuildDataTipText(documentationBuilder, collector, collector, collector, collector, collector, description)
return CompletionDescription.Create(documentation.ToImmutableArray())
else
return CompletionDescription.Empty
| _ ->
return CompletionDescription.Empty
} |> RoslynHelpers.StartAsyncAsTask cancellationToken
override this.GetChangeAsync(document, item, _, cancellationToken) : Task<CompletionChange> =
async {
use _logBlock = Logger.LogBlockMessage document.Name LogEditorFunctionId.Completion_GetChangeAsync
let fullName =
match item.Properties.TryGetValue FullNamePropName with
| true, x -> Some x
| _ -> None
// do not add extension members, keywords and not yet resolved symbols to the MRU list
if not (item.Properties.ContainsKey NamespaceToOpenPropName) && not (item.Properties.ContainsKey IsExtensionMemberPropName) &&
not (item.Properties.ContainsKey IsKeywordPropName) then
match fullName with
| Some fullName ->
match mruItems.TryGetValue fullName with
| true, hints -> mruItems.[fullName] <- hints + 1
| _ -> mruItems.[fullName] <- 1
| _ -> ()
let nameInCode =
match item.Properties.TryGetValue NameInCodePropName with
| true, x -> x
| _ -> item.DisplayText
return!
asyncMaybe {
let! ns =
match item.Properties.TryGetValue NamespaceToOpenPropName with
| true, ns -> Some ns
| _ -> None
let! sourceText = document.GetTextAsync(cancellationToken)
let textWithItemCommitted = sourceText.WithChanges(TextChange(item.Span, nameInCode))
let line = sourceText.Lines.GetLineFromPosition(item.Span.Start)
let! parsingOptions, _options = projectInfoManager.TryGetOptionsForEditingDocumentOrProject(document, cancellationToken)
let! parsedInput = checker.ParseDocument(document, parsingOptions, sourceText, userOpName)
let fullNameIdents = fullName |> Option.map (fun x -> x.Split '.') |> Option.defaultValue [||]
let insertionPoint =
if settings.CodeFixes.AlwaysPlaceOpensAtTopLevel then OpenStatementInsertionPoint.TopLevel
else OpenStatementInsertionPoint.Nearest
let ctx = ParsedInput.findNearestPointToInsertOpenDeclaration line.LineNumber parsedInput fullNameIdents insertionPoint
let finalSourceText, changedSpanStartPos = OpenDeclarationHelper.insertOpenDeclaration textWithItemCommitted ctx ns
let fullChangingSpan = TextSpan.FromBounds(changedSpanStartPos, item.Span.End)
let changedSpan = TextSpan.FromBounds(changedSpanStartPos, item.Span.End + (finalSourceText.Length - sourceText.Length))
let changedText = finalSourceText.ToString(changedSpan)
return CompletionChange.Create(TextChange(fullChangingSpan, changedText)).WithNewPosition(Nullable (changedSpan.End))
}
|> Async.map (Option.defaultValue (CompletionChange.Create(TextChange(item.Span, nameInCode))))
} |> RoslynHelpers.StartAsyncAsTask cancellationToken