-
Notifications
You must be signed in to change notification settings - Fork 790
/
CompletionUtils.fs
235 lines (198 loc) · 8.89 KB
/
CompletionUtils.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
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
namespace Microsoft.VisualStudio.FSharp.Editor
open System.Threading
open Microsoft.CodeAnalysis
open Microsoft.CodeAnalysis.Classification
open Microsoft.CodeAnalysis.Text
open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Completion
open System.Globalization
open FSharp.Compiler.EditorServices
open FSharp.Compiler.Syntax.PrettyNaming
module internal CompletionUtils =
let private isLetterChar (cat: UnicodeCategory) =
// letter-character:
// A Unicode character of classes Lu, Ll, Lt, Lm, Lo, or Nl
// A Unicode-escape-sequence representing a character of classes Lu, Ll, Lt, Lm, Lo, or Nl
match cat with
| UnicodeCategory.UppercaseLetter
| UnicodeCategory.LowercaseLetter
| UnicodeCategory.TitlecaseLetter
| UnicodeCategory.ModifierLetter
| UnicodeCategory.OtherLetter
| UnicodeCategory.LetterNumber -> true
| _ -> false
/// Defines a set of helper methods to classify Unicode characters.
let private isIdentifierStartCharacter (ch: char) =
// identifier-start-character:
// letter-character
// _ (the underscore character U+005F)
if ch < 'a' then // '\u0061'
if ch < 'A' then // '\u0041'
false
else
ch <= 'Z' // '\u005A'
|| ch = '_' // '\u005F'
elif ch <= 'z' then // '\u007A'
true
elif ch <= '\u007F' then // max ASCII
false
else
isLetterChar (CharUnicodeInfo.GetUnicodeCategory(ch))
/// Returns true if the Unicode character can be a part of an identifier.
let private isIdentifierPartCharacter (ch: char) =
// identifier-part-character:
// letter-character
// decimal-digit-character
// connecting-character
// combining-character
// formatting-character
if ch < 'a' then // '\u0061'
if ch < 'A' then // '\u0041'
ch >= '0' // '\u0030'
&& ch <= '9' // '\u0039'
else
ch <= 'Z' // '\u005A'
|| ch = '_' // '\u005F'
elif ch <= 'z' then // '\u007A'
true
elif ch <= '\u007F' then // max ASCII
false
else
let cat = CharUnicodeInfo.GetUnicodeCategory(ch)
isLetterChar (cat)
|| match cat with
| UnicodeCategory.DecimalDigitNumber
| UnicodeCategory.ConnectorPunctuation
| UnicodeCategory.NonSpacingMark
| UnicodeCategory.SpacingCombiningMark -> true
| _ when int ch > 127 -> CharUnicodeInfo.GetUnicodeCategory(ch) = UnicodeCategory.Format
| _ -> false
let isStartingNewWord (sourceText, position) =
FSharpCommonCompletionUtilities.IsStartingNewWord(
sourceText,
position,
(fun ch -> isIdentifierStartCharacter ch),
(fun ch -> isIdentifierPartCharacter ch)
)
let shouldProvideCompletion
(
documentId: DocumentId,
filePath: string,
defines: string list,
langVersion: string option,
strictIndentation: bool option,
sourceText: SourceText,
triggerPosition: int,
ct: CancellationToken
) : bool =
let textLines = sourceText.Lines
let triggerLine = textLines.GetLineFromPosition triggerPosition
let classifiedSpans = ResizeArray<_>()
Tokenizer.classifySpans (
documentId,
sourceText,
triggerLine.Span,
Some filePath,
defines,
langVersion,
strictIndentation,
classifiedSpans,
ct
)
classifiedSpans.Count = 0
|| // we should provide completion at the start of empty line, where there are no tokens at all
let result =
classifiedSpans.Exists(fun classifiedSpan ->
classifiedSpan.TextSpan.IntersectsWith triggerPosition
&& (match classifiedSpan.ClassificationType with
| ClassificationTypeNames.Comment
| ClassificationTypeNames.StringLiteral
| ClassificationTypeNames.ExcludedCode
| ClassificationTypeNames.Operator
| ClassificationTypeNames.NumericLiteral -> false
| _ -> true) // anything else is a valid classification type
) in
result
let inline getKindPriority kind =
match kind with
| CompletionItemKind.SuggestedName
| CompletionItemKind.CustomOperation -> 0
| CompletionItemKind.Property -> 1
| CompletionItemKind.Field -> 2
| CompletionItemKind.Method(isExtension = false) -> 3
| CompletionItemKind.Event -> 4
| CompletionItemKind.Argument -> 5
| CompletionItemKind.Other -> 6
| CompletionItemKind.Method(isExtension = true) -> 7
/// Indicates the text span to be replaced by a committed completion list item.
let getDefaultCompletionListSpan
(
sourceText: SourceText,
caretIndex,
documentId,
filePath,
defines,
langVersion,
strictIndentation,
ct: CancellationToken
) =
// Gets connected identifier-part characters backward and forward from caret.
let getIdentifierChars () =
let mutable startIndex = caretIndex
let mutable endIndex = caretIndex
while startIndex > 0 && IsIdentifierPartCharacter sourceText.[startIndex - 1] do
startIndex <- startIndex - 1
if startIndex <> caretIndex then
while endIndex < sourceText.Length && IsIdentifierPartCharacter sourceText.[endIndex] do
endIndex <- endIndex + 1
TextSpan.FromBounds(startIndex, endIndex)
let line = sourceText.Lines.GetLineFromPosition(caretIndex)
if line.ToString().IndexOf "``" < 0 then
// No backticks on the line, capture standard identifier chars.
getIdentifierChars ()
else
// Line contains backticks.
// Use tokenizer to check for identifier, in order to correctly handle extraneous backticks in comments, strings, etc.
// If caret is at a backtick-identifier, then that is our span.
// Else, check if we are after an unclosed ``, to support the common case of a manually typed leading ``.
// Else, backticks are not involved in caret location, fall back to standard identifier character scan.
// There may still be edge cases where backtick related spans are incorrectly captured, such as unclosed
// backticks before later valid backticks on a line, this is an acceptable compromise in order to support
// the majority of common cases.
let classifiedSpans = ResizeArray<_>()
Tokenizer.classifySpans (
documentId,
sourceText,
line.Span,
Some filePath,
defines,
langVersion,
strictIndentation,
classifiedSpans,
ct
)
let inline isBacktickIdentifier (classifiedSpan: ClassifiedSpan) =
classifiedSpan.ClassificationType = ClassificationTypeNames.Identifier
&& Tokenizer.isDoubleBacktickIdent (sourceText.ToString(classifiedSpan.TextSpan))
let inline isUnclosedBacktick (classifiedSpan: ClassifiedSpan) =
classifiedSpan.ClassificationType = ClassificationTypeNames.Identifier
&& sourceText.ToString(classifiedSpan.TextSpan).StartsWith "``"
match
classifiedSpans
|> Seq.tryFindV (fun cs -> isBacktickIdentifier cs && cs.TextSpan.IntersectsWith caretIndex)
with
| ValueSome backtickIdentifier ->
// Backtick enclosed identifier found intersecting with caret, use its span.
backtickIdentifier.TextSpan
| _ ->
match
classifiedSpans
|> Seq.tryFindBack (fun cs -> isUnclosedBacktick cs && caretIndex >= cs.TextSpan.Start)
with
| Some unclosedBacktick ->
// Unclosed backtick found before caret, use span from backtick to end of line.
let lastSpan = classifiedSpans.[classifiedSpans.Count - 1]
TextSpan.FromBounds(unclosedBacktick.TextSpan.Start, lastSpan.TextSpan.End)
| _ ->
// No backticks involved at caret position, fall back to standard identifier chars.
getIdentifierChars ()