-
Notifications
You must be signed in to change notification settings - Fork 155
/
InlayHints.fs
1023 lines (883 loc) · 37.8 KB
/
InlayHints.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
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
module FsAutoComplete.Core.InlayHints
open System
open FSharp.Compiler.Text
open FSharp.Compiler.Syntax
open FsToolkit.ErrorHandling
open FsAutoComplete
open FSharp.Compiler.Symbols
open FSharp.UMX
open System.Linq
open System.Collections.Immutable
open FSharp.Compiler.CodeAnalysis
open FSharp.Compiler.Text.Range
open FsAutoComplete.Core.Workaround.ServiceParseTreeWalk
/// `traversePat`from `SyntaxTraversal.Traverse`
///
/// Reason for extra function:
/// * can be used to traverse when traversal isn't available via `defaultTraverse` (for example: in `VisitExpr`, and want traverse a `SynPat`)
/// * visits `SynPat.Record(fieldPats)`
///
/// Note: doesn't visit `SynPat.Typed(targetType)`: requires traversal into `SynType` (`SynPat.Typed(pat)` gets visited!)
let rec private traversePat (visitor: SyntaxVisitorBase<_>) origPath pat =
let defaultTraverse = defaultTraversePat visitor origPath
visitor.VisitPat(origPath, defaultTraverse, pat)
and private defaultTraversePat visitor origPath pat =
let path = SyntaxNode.SynPat pat :: origPath
match pat with
| SynPat.Paren (p, _) -> traversePat visitor path p
| SynPat.As (p1, p2, _)
| SynPat.Or (p1, p2, _, _) -> [ p1; p2 ] |> List.tryPick (traversePat visitor path)
| SynPat.Ands (ps, _)
| SynPat.Tuple (_, ps, _)
| SynPat.ArrayOrList (_, ps, _) -> ps |> List.tryPick (traversePat visitor path)
| SynPat.Attrib (p, _, _) -> traversePat visitor path p
| SynPat.LongIdent (argPats = args) ->
match args with
| SynArgPats.Pats ps -> ps |> List.tryPick (traversePat visitor path)
| SynArgPats.NamePatPairs (ps, _) ->
ps
|> List.map (fun (_, _, pat) -> pat)
|> List.tryPick (traversePat visitor path)
| SynPat.Typed (p, _ty, _) -> traversePat visitor path p
// no access to `traverseSynType` -> no traversing into `ty`
| SynPat.Record (fieldPats = fieldPats) ->
fieldPats
|> List.map (fun (_, _, pat) -> pat)
|> List.tryPick (traversePat visitor path)
| _ -> None
type HintKind =
| Parameter
| Type
type HintInsertion = { Pos: Position; Text: string }
type Hint =
{ IdentRange: Range
Kind: HintKind
Pos: Position
Text: string
Insertions: HintInsertion[] option
//ENHANCEMENT: allow xml doc
Tooltip: string option }
let private getArgumentsFor (state: FsAutoComplete.State, p: ParseAndCheckResults, identText: Range) =
option {
let! contents = state.TryGetFileSource p.FileName |> Option.ofResult
let! line = contents.GetLine identText.End
let! symbolUse = p.TryGetSymbolUse identText.End line
match symbolUse.Symbol with
| :? FSharpMemberOrFunctionOrValue as mfv when
mfv.IsFunction || mfv.IsConstructor || mfv.CurriedParameterGroups.Count <> 0
->
let parameters = mfv.CurriedParameterGroups
let formatted =
parameters
|> Seq.collect (fun pGroup -> pGroup |> Seq.map (fun p -> p.DisplayName + ":"))
return formatted |> Array.ofSeq
| _ -> return! None
}
let private isSignatureFile (f: string<LocalPath>) =
System.IO.Path.GetExtension(UMX.untag f) = ".fsi"
type private FSharp.Compiler.CodeAnalysis.FSharpParseFileResults with
// duplicates + extends the logic in FCS to match bindings of the form `let x: int = 12`
// so that they are considered logically the same as a 'typed' SynPat
member x.IsTypeAnnotationGivenAtPositionPatched pos =
let visitor: SyntaxVisitorBase<Range> =
{ new SyntaxVisitorBase<_>() with
override _.VisitExpr(_path, _traverseSynExpr, defaultTraverse, expr) =
match expr with
| SynExpr.Typed (_expr, _typeExpr, range) when Position.posEq range.Start pos -> Some range
| _ -> defaultTraverse expr
override _.VisitSimplePats(_path, pats) =
match pats with
| [] -> None
| _ ->
let exprFunc pat =
match pat with
| SynSimplePat.Typed (_pat, _targetExpr, range) when Position.posEq range.Start pos -> Some range
| _ -> None
pats |> List.tryPick exprFunc
override visitor.VisitPat(path, defaultTraverse, pat) =
match pat with
| SynPat.Typed (_pat, _targetType, range) when Position.posEq range.Start pos -> Some range
| _ -> defaultTraversePat visitor path pat
override _.VisitBinding(_path, defaultTraverse, binding) =
match binding with
| SynBinding (headPat = SynPat.Named (range = patRange)
returnInfo = Some (SynBindingReturnInfo(typeName = SynType.LongIdent (idents)))) ->
Some patRange
| _ -> defaultTraverse binding }
let result = SyntaxTraversal.Traverse(pos, x.ParseTree, visitor)
result.IsSome
let private getFirstPositionAfterParen (str: string) startPos =
match str with
| null -> -1
| str when startPos > str.Length -> -1
| str -> str.IndexOf('(') + 1
let private maxHintLength = 30
let inline private shouldTruncate (s: string) = s.Length > maxHintLength
let inline private tryTruncate (s: string) =
if shouldTruncate s then
s.Substring(0, maxHintLength) + "..." |> Some
else
None
let truncated (s: string) = tryTruncate s |> Option.defaultValue s
let private createParamHint (identRange: Range) (paramName: string) =
let (truncated, tooltip) =
match tryTruncate paramName with
| None -> (paramName, None)
| Some truncated -> (truncated, Some paramName)
{ IdentRange = identRange
Pos = identRange.Start
Kind = Parameter
Text = truncated + " ="
Insertions = None
Tooltip = tooltip }
module private ShouldCreate =
let private isNotWellKnownName =
let names = Set.ofList [ "value"; "x" ]
fun (p: FSharpParameter) ->
match p.Name with
| None -> true
| Some n -> not (Set.contains n names)
[<return: Struct>]
let private (|StartsWith|_|) (v: string) (fullName: string) =
if fullName.StartsWith v then ValueSome() else ValueNone
// doesn't differentiate between modules, types, namespaces
// -> is just for documentation in code
[<return: Struct>]
let private (|Module|_|) = (|StartsWith|_|)
[<return: Struct>]
let private (|Type|_|) = (|StartsWith|_|)
[<return: Struct>]
let private (|Namespace|_|) = (|StartsWith|_|)
let private commonCollectionParams =
Set.ofList
[ "mapping"
"projection"
"chooser"
"value"
"predicate"
"folder"
"state"
"initializer"
"action"
"list"
"array"
"source"
"lists"
"arrays"
"sources" ]
let private isWellKnownParameterOrFunction (func: FSharpMemberOrFunctionOrValue) (param: FSharpParameter) =
match func.FullName with
| Module "Microsoft.FSharp.Core.Option" ->
// don't show param named `option`, but other params for Option
match param.Name with
| Some "option" -> true
| _ -> false
| Module "Microsoft.FSharp.Core.ValueOption" ->
match param.Name with
| Some "voption" -> true
| _ -> false
| Module "Microsoft.FSharp.Core.ExtraTopLevelOperators" // only printf-members have `format`
| Module "Microsoft.FSharp.Core.Printf" ->
// don't show param named `format`
match param.Name with
| Some "format" -> true
| _ -> false
| Namespace "Microsoft.FSharp.Collections" ->
match param.Name with
| Some name -> commonCollectionParams |> Set.contains name
| _ -> false
| _ -> false
let inline private hasName (p: FSharpParameter) =
not (String.IsNullOrEmpty p.DisplayName) && p.DisplayName <> "````"
let inline private isMeaningfulName (p: FSharpParameter) = p.DisplayName.Length > 2
let inline private isOperator (func: FSharpMemberOrFunctionOrValue) = func.CompiledName.StartsWith "op_"
/// Doesn't consider lower/upper cases:
/// * `areSame "foo" "FOO" = true`
/// * `areSame "Foo" "Foo" = true`
let inline private areSame (a: ReadOnlySpan<char>) (b: ReadOnlySpan<char>) =
a.Equals(b, StringComparison.OrdinalIgnoreCase)
/// Boundary checks:
/// * word boundary (-> upper case letter)
/// `"foo" |> isPrefixOf "fooBar"`
/// Doesn't consider capitalization, except for word boundary after prefix:
/// * `foo` prefix of `fooBar`
/// * `foo` not prefix of `foobar`
let inline private isPrefixOf (root: ReadOnlySpan<char>) (check: ReadOnlySpan<char>) =
root.StartsWith(check, StringComparison.OrdinalIgnoreCase)
&& (
// same
root.Length <= check.Length
||
// rest must start with upper case -> new word
Char.IsUpper root[check.Length])
/// Boundary checks:
/// * word boundary (-> upper case letter)
/// `"bar" |> isPostifxOf "fooBar"`
/// * `.` boundary (-> property access)
/// `"bar" |> isPostifxOf "data.bar"`
///
/// Doesn't consider capitalization, except for word boundary at start of postfix:
/// * `bar` postfix of `fooBar`
/// * `bar` not postfix of `foobar`
let inline private isPostfixOf (root: ReadOnlySpan<char>) (check: ReadOnlySpan<char>) =
root.EndsWith(check, StringComparison.OrdinalIgnoreCase)
&& (root.Length <= check.Length
||
// postfix must start with upper case -> word boundary
Char.IsUpper root[root.Length - check.Length])
let inline private removeLeadingUnderscore (name: ReadOnlySpan<char>) = name.TrimStart '_'
let inline private removeTrailingTick (name: ReadOnlySpan<char>) = name.TrimEnd '\''
let inline private extractLastIdentifier (name: ReadOnlySpan<char>) =
// exclude backticks for now: might contain `.` -> difficult to split
if name.StartsWith "``" || name.EndsWith "``" then
name
else
match name.LastIndexOf '.' with
| -1 -> name
| i -> name.Slice(i + 1)
/// Note: when in parens: might not be an identifier, but expression!
///
/// Note: might result in invalid expression (because no matching parens `string (2)` -> `string (2`)
let inline private trimParensAndSpace (name: ReadOnlySpan<char>) = name.TrimStart("( ").TrimEnd(" )")
/// Note: including `.`
let inline private isLongIdentifier (name: ReadOnlySpan<char>) =
// name |> Seq.forall PrettyNaming.IsLongIdentifierPartCharacter
let mutable valid = true
let mutable i = 0
while valid && i < name.Length do
if PrettyNaming.IsLongIdentifierPartCharacter name[i] then
i <- i + 1
else
valid <- false
valid
let private areSimilar (paramName: string) (argumentText: string) =
// no pipe with span ...
let paramName = removeTrailingTick (removeLeadingUnderscore (paramName.AsSpan()))
let argumentName =
let argumentText = argumentText.AsSpan()
let argTextNoParens = trimParensAndSpace argumentText
if isLongIdentifier argTextNoParens then
removeTrailingTick (extractLastIdentifier argTextNoParens)
else
argumentText
// special case: argumentText is empty string. Happens for unit (`()`)
if argumentName.IsWhiteSpace() then
false
else
// // covered by each isPre/PostfixOf
// areSame paramName argumentName
// ||
isPrefixOf argumentName paramName
|| isPostfixOf argumentName paramName
|| isPrefixOf paramName argumentName
|| isPostfixOf paramName argumentName
let inline private doesNotMatchArgumentText (parameterName: string) (userArgumentText: string) =
parameterName <> userArgumentText
&& not (userArgumentText.StartsWith parameterName)
let private isParamNamePostfixOfFuncName (func: FSharpMemberOrFunctionOrValue) (paramName: string) =
let funcName = func.DisplayName.AsSpan()
let paramName = removeLeadingUnderscore (paramName.AsSpan())
isPostfixOf funcName paramName
/// </summary>
/// We filter out parameters that generate lots of noise in hints.
/// * parameter has no name
/// * parameter has length > 2
/// * parameter is one of a set of 'known' names that clutter (like printfn formats)
/// * param & function is "well known"/commonly used
/// * parameter does match or is a pre/postfix of user-entered text
/// * user-entered text does match or is a pre/postfix of parameter
/// * parameter is postfix of function name
/// </summary>
let paramHint (func: FSharpMemberOrFunctionOrValue) (p: FSharpParameter) (argumentText: string) =
hasName p
&& isMeaningfulName p
&& isNotWellKnownName p
&& (not (isWellKnownParameterOrFunction func p))
&& (not (isOperator func))
&& (not (areSimilar p.DisplayName argumentText))
&& (not (isParamNamePostfixOfFuncName func p.DisplayName))
type TypeName = string
type TypeNameForAnnotation = TypeName
type SpecialRule =
/// For Optional: `?v` -> `?v: int`, NOT `v: int option`
/// And parens must include optional, not just `v`
| RemoveOptionFromType
type SpecialRules = SpecialRule list
[<RequireQualifiedAccess>]
type Parens =
| Forbidden
/// Technically `Optional` too: Usually additional parens are ok
///
/// Note: `additionalParens` are inside of existing parens:
/// `(|ident|)`
/// * `()`: existing parens
/// * `||`: additional parens location
| Exist of additionalParens: Range
| Optional of Range
| Required of Range
type MissingExplicitType =
{ Ident: Range
InsertAt: Position
Parens: Parens
SpecialRules: SpecialRules }
type MissingExplicitType with
/// <returns>
/// * type name
/// * type name formatted with `SpecialRules`
/// -> to use as type annotation
/// </returns>
member x.FormatType(ty: FSharpType, displayContext: FSharpDisplayContext) : TypeName * TypeNameForAnnotation =
let typeName = ty.Format displayContext
let anno =
if x.SpecialRules |> List.contains RemoveOptionFromType then
// Optional parameter:
// `static member F(?a) =` -> `: int`, NOT `: int option`
if typeName.EndsWith " option" then
typeName.Substring(0, typeName.Length - " option".Length)
else
typeName
else
typeName
(typeName, anno)
member x.CreateEdits(typeForAnnotation) =
[| match x.Parens with
| Parens.Required range -> { Pos = range.Start; Text = "(" }
| _ -> ()
{ Pos = x.InsertAt; Text = ": " }
{ Pos = x.InsertAt
Text = typeForAnnotation }
match x.Parens with
| Parens.Required range -> { Pos = range.End; Text = ")" }
| _ -> () |]
member x.TypeAndEdits(ty: FSharpType, displayContext: FSharpDisplayContext) =
let (ty, tyForAnntotation) = x.FormatType(ty, displayContext)
let edits = x.CreateEdits(tyForAnntotation)
(ty, edits)
/// Note: No validation of `mfv`!
member x.TypeAndEdits(mfv: FSharpMemberOrFunctionOrValue, displayContext: FSharpDisplayContext) =
x.TypeAndEdits(mfv.FullType, displayContext)
/// Note: Missing considers only directly typed, not parently (or ancestorly) typed:
/// ```fsharp
/// let (value: int, _) = (1,2)
/// // ^^^^^ directly typed -> Exists
/// let (value,_): int*int = (1,2)
/// // ^^^ parently typed -> Missing
/// ```
[<RequireQualifiedAccess>]
type ExplicitType =
/// in for loop (only indent allowed -- nothing else (neither type nor parens))
| Invalid
| Exists
| Missing of MissingExplicitType
| Debug of string
type ExplicitType with
member x.TryGetTypeAndEdits(ty: FSharpType, displayContext: FSharpDisplayContext) =
match x with
| ExplicitType.Missing data -> data.TypeAndEdits(ty, displayContext) |> Some
| _ -> None
/// Type Annotation must be directly for identifier, not somewhere up the line:
/// `v: int` -> directly typed
/// `(v,_): int*int` -> parently typed
///
/// Still considered directly typed:
/// * Parentheses: `(v): int`
/// * Attributes: `([<Attr>]v): int`
let rec private isDirectlyTyped (identStart: Position) (path: SyntaxVisitorPath) =
//ENHANCEMENT: handle SynExpr.Typed? -> not at binding, but usage
match path with
| [] -> false
| SyntaxNode.SynPat (SynPat.Typed (pat = pat)) :: _ when rangeContainsPos pat.Range identStart -> true
| SyntaxNode.SynPat (SynPat.Paren _) :: path -> isDirectlyTyped identStart path
| SyntaxNode.SynPat (SynPat.Attrib (pat = pat)) :: path when rangeContainsPos pat.Range identStart ->
isDirectlyTyped identStart path
| SyntaxNode.SynBinding (SynBinding (headPat = headPat; returnInfo = Some (SynBindingReturnInfo _))) :: _ when
rangeContainsPos headPat.Range identStart
->
true
| SyntaxNode.SynExpr (SynExpr.Paren _) :: path -> isDirectlyTyped identStart path
| SyntaxNode.SynExpr (SynExpr.Typed (expr = expr)) :: _ when rangeContainsPos expr.Range identStart -> true
| _ -> false
/// Note: FULL range of pattern -> everything in parens
/// For `SynPat.Named`: Neither `range` nor `ident.idRange` span complete range: Neither includes Accessibility:
/// `let private (a: int)` is not valid, must include private: `let (private a: int)`
let rec private getParensForPatternWithIdent (patternRange: Range) (identStart: Position) (path: SyntaxVisitorPath) =
match path with
| SyntaxNode.SynPat (SynPat.Paren _) :: _ ->
// (x)
Parens.Exist patternRange
| SyntaxNode.SynBinding (SynBinding (headPat = headPat)) :: _ when rangeContainsPos headPat.Range identStart ->
// let x =
Parens.Optional patternRange
| SyntaxNode.SynPat (SynPat.Tuple(isStruct = true)) :: _ ->
// struct (x,y)
Parens.Optional patternRange
| SyntaxNode.SynPat (SynPat.Tuple _) :: SyntaxNode.SynPat (SynPat.Paren _) :: _ ->
// (x,y)
Parens.Optional patternRange
| SyntaxNode.SynPat (SynPat.Tuple _) :: _ ->
// x,y
Parens.Required patternRange
| SyntaxNode.SynPat (SynPat.ArrayOrList _) :: _ ->
// [x;y;z]
Parens.Optional patternRange
| SyntaxNode.SynPat (SynPat.As _) :: SyntaxNode.SynPat (SynPat.Paren _) :: _ -> Parens.Optional patternRange
| SyntaxNode.SynPat (SynPat.As (rhsPat = pat)) :: SyntaxNode.SynBinding (SynBinding (headPat = headPat)) :: _ when
rangeContainsPos pat.Range identStart
&& rangeContainsPos headPat.Range identStart
->
// let _ as value =
// ->
// let _ as value: int =
// (new `: int` belongs to let binding, NOT as pattern)
Parens.Optional patternRange
| SyntaxNode.SynPat (SynPat.As (lhsPat = pat)) :: SyntaxNode.SynBinding (SynBinding (headPat = headPat)) :: _ when
rangeContainsPos pat.Range identStart
&& rangeContainsPos headPat.Range identStart
->
// let value as _ =
// ->
// let (value: int) as _ =
// (`: int` belongs to as pattern, but let bindings tries to parse type annotation eagerly -> without parens let binding finished after `: int` -> as not pattern)
Parens.Required patternRange
| SyntaxNode.SynPat (SynPat.As (rhsPat = pat)) :: _ when rangeContainsPos pat.Range identStart ->
// _ as (value: int)
Parens.Required patternRange
| SyntaxNode.SynPat (SynPat.As (lhsPat = pat)) :: _ when rangeContainsPos pat.Range identStart ->
// value: int as _
// ^^^^^^^^^^ unlike rhs this here doesn't require parens...
Parens.Optional patternRange
| SyntaxNode.SynPat (SynPat.Record _) :: _ ->
// { Value=value }
Parens.Optional patternRange
| SyntaxNode.SynPat (SynPat.LongIdent(argPats = SynArgPats.NamePatPairs (range = range))) :: _ when
rangeContainsPos range identStart
->
// U (Value=value)
// ^ ^
// must exist to be valid
Parens.Optional patternRange
| SyntaxNode.SynExpr (SynExpr.LetOrUseBang(isUse = true)) :: _ ->
// use! x =
// Note: Type is forbidden too...
Parens.Forbidden
| SyntaxNode.SynExpr (SynExpr.LetOrUseBang(isUse = false)) :: _ ->
// let! x =
Parens.Required patternRange
| SyntaxNode.SynExpr (SynExpr.ForEach _) :: _ ->
// for i in [1..4] do
Parens.Optional patternRange
| []
| _ -> Parens.Required patternRange
/// Gets range of `SynPat.Named`
///
/// Issue with range of `SynPat.Named`:
/// `pat.range` only covers ident (-> `= ident.idRange`),
/// not `accessibility`.
///
/// Note: doesn't handle when accessibility is on prev line
let private rangeOfNamedPat (text: NamedText) (pat: SynPat) =
match pat with
| SynPat.Named(accessibility = None) -> pat.Range
| SynPat.Named (ident = ident; accessibility = Some (access)) ->
maybe {
let start = ident.idRange.Start
let! line = text.GetLine start
let access = access.ToString().ToLowerInvariant().AsSpan()
// word before ident must be access
let pre = line.AsSpan(0, start.Column)
match pre.LastIndexOf(access) with
| -1 -> return! None
| c ->
// must be directly before ident
let word = pre.Slice(c).TrimEnd()
if word.Length = access.Length then
let start = Position.mkPos start.Line c
let range =
let range = ident.idRange
Range.mkRange range.FileName start range.End
return range
else
return! None
}
|> Option.defaultValue pat.Range
| _ -> failwith "Pattern must be Named!"
/// Note: (deliberately) fails when `pat` is neither `Named` nor `OptionalVal`
let rec private getParensForIdentPat (text: NamedText) (pat: SynPat) (path: SyntaxVisitorPath) =
match pat with
| SynPat.Named (ident = ident) ->
// neither `range`, not `pat.Range` includes `accessibility`...
// `let private (a: int)` is not valid, must include private: `let (private a: int)`
let patternRange = rangeOfNamedPat text pat
let identStart = ident.idRange.Start
getParensForPatternWithIdent patternRange identStart path
| SynPat.OptionalVal (ident = ident) ->
let patternRange = pat.Range
let identStart = ident.idRange.Start
getParensForPatternWithIdent patternRange identStart path
| _ -> failwith "Pattern must be Named or OptionalVal!"
let tryGetExplicitTypeInfo (text: NamedText, ast: ParsedInput) (pos: Position) : ExplicitType option =
SyntaxTraversal.Traverse(
pos,
ast,
{ new SyntaxVisitorBase<_>() with
member x.VisitExpr(path, traverseSynExpr, defaultTraverse, expr) =
match expr with
// special case:
// for loop:
// for i = 1 to 3 do
// ^ -> just Ident (neither SynPat nor SynSimplePat)
// -> no type allowed (not even parens)...
| SynExpr.For (ident = ident) when rangeContainsPos ident.idRange pos -> ExplicitType.Invalid |> Some
| SynExpr.Lambda(parsedData = Some (args, body)) ->
// original visitor walks down `SynExpr.Lambda(args; body)`
// Issue:
// `args` are `SynSimplePats` -> no complex pattern
// When pattern: is in body. In `args` then generated Identifier:
// * `let f1 = fun v -> v + 1`
// -> `v` is in `args` (-> SynSimplePat)
// * `let f2 = fun (Value v) -> v + 1`
// -> compiler generated `_arg1` in `args`,
// and `v` is inside match expression in `body` & `parsedData` (-> `SynPat` )
// -> unify by looking into `parsedData` (-> args & body):
// -> `parsedData |> fst` contains `args` as `SynPat`
//TODO: always correct?
let arg = args |> List.tryFind (fun pat -> rangeContainsPos pat.Range pos)
if arg |> Option.isSome then
let pat = arg.Value
traversePat x (SyntaxNode.SynExpr(expr) :: path) pat
elif rangeContainsPos body.Range pos then
traverseSynExpr body
else
None
| _ -> defaultTraverse expr
member visitor.VisitPat(path, defaultTraverse, pat) =
let invalidPositionForTypeAnnotation (pos: Position) (path: SyntaxNode list) =
match path with
| SyntaxNode.SynExpr (SynExpr.LetOrUseBang(isUse = true)) :: _ ->
// use! value =
true
| _ -> false
//ENHANCEMENT: differentiate between directly typed and parently typed?
// (maybe even further ancestorly typed?)
// ```fsharp
// let (a: int,b) = (1,2)
// // ^^^ directly typed
// let (a,b): int*int = (1,2)
// // ^^^ parently typed
// ```
// currently: only directly typed is typed
match pat with
// no simple way out: Range for `SynPat.LongIdent` doesn't cover full pats (just ident)
// see dotnet/fsharp#13115
// | _ when not (rangeContainsPos pat.Range pos) -> None
| SynPat.Named (ident = ident) when
rangeContainsPos ident.idRange pos && invalidPositionForTypeAnnotation pos path
->
ExplicitType.Invalid |> Some
| SynPat.Named (ident = ident; isThisVal = false) when rangeContainsPos ident.idRange pos ->
let typed = isDirectlyTyped ident.idRange.Start path
if typed then
ExplicitType.Exists |> Some
else
let parens = getParensForIdentPat text pat path
ExplicitType.Missing
{ Ident = ident.idRange
InsertAt = ident.idRange.End
Parens = parens
SpecialRules = [] }
|> Some
| SynPat.OptionalVal (ident = ident) when rangeContainsPos ident.idRange pos ->
let typed = isDirectlyTyped ident.idRange.Start path
if typed then
ExplicitType.Exists |> Some
else
let parens = getParensForIdentPat text pat path
ExplicitType.Missing
{ Ident = ident.idRange
InsertAt = ident.idRange.End
Parens = parens
SpecialRules = [ RemoveOptionFromType ]
// ^^^^^^^^^^^^^^^^^^^^
// `?v: int`, NOT `?v: int option`
}
|> Some
| _ -> defaultTraversePat visitor path pat
member _.VisitSimplePats(path, pats) =
// SynSimplePats at:
// * Primary ctor:
// * SynMemberDefn.ImplicitCtor.ctorArgs
// * SynTypeDefnSimpleRepr.General.implicitCtorSynPats
// * Lambda: SynExpr.Lambda.args
// * issue: might or might not be actual identifier
// * `let f1 = fun v -> v + 1`
// -> `v` is in `args` (-> SynSimplePat)
// * `let f2 = fun (Value v) -> v + 1`
// -> compiler generated `_arg1` in `args`,
// and `v` is inside match expression in `body` & `parsedData` (-> `SynPat` )
maybe {
let! pat = pats |> List.tryFind (fun p -> rangeContainsPos p.Range pos)
let rec tryGetIdent pat =
match pat with
| SynSimplePat.Id (ident = ident) when rangeContainsPos ident.idRange pos -> Some pat
| SynSimplePat.Attrib (pat = pat) when rangeContainsPos pat.Range pos -> tryGetIdent pat
| SynSimplePat.Typed (pat = pat) when rangeContainsPos pat.Range pos -> tryGetIdent pat
| _ -> None
let! ident = tryGetIdent pat
match ident with
| SynSimplePat.Id(isCompilerGenerated = false) ->
let rec isTyped =
function
| SynSimplePat.Typed _ -> true
| SynSimplePat.Id _ -> false
| SynSimplePat.Attrib (pat = pat) -> isTyped pat
let typed = isTyped pat
if typed then
return ExplicitType.Exists
else
let isCtor =
path
|> List.tryHead
|> Option.map (function
// normal ctor in type: `type A(v) = ...`
| SyntaxNode.SynMemberDefn (SynMemberDefn.ImplicitCtor _) -> true
//TODO: when? example?
| SyntaxNode.SynTypeDefn (SynTypeDefn(typeRepr = SynTypeDefnRepr.Simple(simpleRepr = SynTypeDefnSimpleRepr.General(implicitCtorSynPats = Some (ctorPats))))) when
rangeContainsPos ctorPats.Range pos
->
true
| _ -> false)
|> Option.defaultValue false
if isCtor then
return
ExplicitType.Missing
{ Ident = ident.Range
InsertAt = ident.Range.End
Parens = Parens.Forbidden
SpecialRules = [] }
else
// lambda
return! None
| _ -> return! None
} }
)
///<returns>
/// List of all curried params.
///
/// For each curried param:
/// * its range (including params)
/// * range of tupled params
/// * Note: one range when no tuple
///
/// ```fsharp
/// f alpha (beta, gamma)
/// ```
/// ->
/// ```fsharp
/// [
/// (2:7, [2:7])
/// (8:21, [9:13; 15:20])
/// ]
/// ```
///</returns>
let private getArgRangesOfFunctionApplication (ast: ParsedInput) pos =
SyntaxTraversal.Traverse(
pos,
ast,
{ new SyntaxVisitorBase<_>() with
member _.VisitExpr(_, traverseSynExpr, defaultTraverse, expr) =
match expr with
| SynExpr.App (isInfix = false; funcExpr = funcExpr; argExpr = argExpr; range = range) when pos = range.Start ->
let isInfixFuncExpr =
match funcExpr with
| SynExpr.App (_, isInfix, _, _, _) -> isInfix
| _ -> false
if isInfixFuncExpr then
traverseSynExpr funcExpr
else
let rec withoutParens =
function
| SynExpr.Paren (expr = expr) -> withoutParens expr
| expr -> expr
// f a (b,c)
// ^^^^^^^^^ App
// ... func
// ----- arg
// ^^^ App
// . func
// - arg
let rec findArgs expr =
match expr with
| SynExpr.Const(constant = SynConst.Unit) -> []
| SynExpr.Paren (expr = expr) -> findArgs expr
| SynExpr.App (funcExpr = funcExpr; argExpr = argExpr) ->
let otherArgRanges = findArgs funcExpr
let argRange =
let argRange = argExpr.Range
let tupleArgs =
match argExpr |> withoutParens with
| SynExpr.Tuple (exprs = exprs) -> exprs |> List.map (fun e -> e.Range)
| _ -> argRange |> List.singleton
(argRange, tupleArgs)
argRange :: otherArgRanges
| _ -> []
findArgs expr |> Some
| _ -> defaultTraverse expr }
)
|> Option.map List.rev
/// Note: No exhausting check. Doesn't check for:
/// * is already typed (-> done by getting `ExplicitType`)
/// * Filters like excluding functions (vs. lambda functions)
/// * `mfv.IsFromDefinition`
///
/// `allowFunctionValues`: `let f = fun a b -> a + b`
/// -> enabled: `f` is target
/// Note: NOT actual functions with direct parameters:
/// `let f a b = a + b` -> `f` isn't target
/// Note: can be parameters too:
/// `let map f v = f v` -> `f` is target
let isPotentialTargetForTypeAnnotation
(allowFunctionValues: bool)
(symbolUse: FSharpSymbolUse, mfv: FSharpMemberOrFunctionOrValue)
=
//ENHANCEMENT: extract settings
(mfv.IsValue || (allowFunctionValues && mfv.IsFunction))
&& not (
mfv.IsMember
|| mfv.IsMemberThisValue
|| mfv.IsConstructorThisValue
|| PrettyNaming.IsOperatorDisplayName mfv.DisplayName
)
let tryGetDetailedExplicitTypeInfo
(isValidTarget: FSharpSymbolUse * FSharpMemberOrFunctionOrValue -> bool)
(text: NamedText, parseAndCheck: ParseAndCheckResults)
(pos: Position)
=
maybe {
let! line = text.GetLine pos
let! symbolUse = parseAndCheck.TryGetSymbolUse pos line
match symbolUse.Symbol with
| :? FSharpMemberOrFunctionOrValue as mfv when isValidTarget (symbolUse, mfv) ->
let! explTy = tryGetExplicitTypeInfo (text, parseAndCheck.GetAST) pos
return (symbolUse, mfv, explTy)
| _ -> return! None
}
let private tryCreateTypeHint (explicitType: ExplicitType) (ty: FSharpType) (displayContext: FSharpDisplayContext) =
match explicitType with
| ExplicitType.Missing data ->
let (ty, tyForAnno) = data.FormatType(ty, displayContext)
let (truncated, tooltip) =
match tryTruncate ty with
| None -> (ty, None)
| Some truncated -> (truncated, Some ty)
{ IdentRange = data.Ident
Pos = data.InsertAt
Kind = Type
// TODO: or use tyForAnno?: `?value: int`, but type is `int option`
Text = ": " + truncated
Insertions = Some <| data.CreateEdits tyForAnno
Tooltip = tooltip }
|> Some
| _ -> None
type HintConfig =
{ ShowTypeHints: bool
ShowParameterHints: bool }
let provideHints (text: NamedText, parseAndCheck: ParseAndCheckResults, range: Range, hintConfig) : Async<Hint[]> =
asyncResult {
let! cancellationToken = Async.CancellationToken
let symbolUses =
parseAndCheck.GetCheckResults.GetAllUsesOfAllSymbolsInFile(cancellationToken)
|> Seq.filter (fun su -> rangeContainsRange range su.Range)
let typeHints = ImmutableArray.CreateBuilder()
let parameterHints = ImmutableArray.CreateBuilder()
for symbolUse in symbolUses do
match symbolUse.Symbol with
| :? FSharpMemberOrFunctionOrValue as mfv when
hintConfig.ShowTypeHints
&& symbolUse.IsFromDefinition
&& isPotentialTargetForTypeAnnotation false (symbolUse, mfv)
->
tryGetExplicitTypeInfo (text, parseAndCheck.GetAST) symbolUse.Range.Start
|> Option.bind (fun explTy -> tryCreateTypeHint explTy mfv.FullType symbolUse.DisplayContext)
|> Option.iter typeHints.Add
| :? FSharpMemberOrFunctionOrValue as func when
hintConfig.ShowParameterHints
&& func.IsFunction
&& not symbolUse.IsFromDefinition
->
let curriedParamGroups = func.CurriedParameterGroups
let appliedArgRanges =
getArgRangesOfFunctionApplication parseAndCheck.GetAST symbolUse.Range.Start
|> Option.defaultValue []
for (def, (appliedArgRange, tupleRanges)) in Seq.zip curriedParamGroups appliedArgRanges do
assert (def.Count > 0)
match tupleRanges with
| _ when def.Count = 1 ->
// single param at def
let p = def[0]
let! appliedArgText = text[appliedArgRange]
if ShouldCreate.paramHint func p appliedArgText then
let defArgName = p.DisplayName
let hint = createParamHint appliedArgRange defArgName
parameterHints.Add hint
| [ _ ] ->
// single param at app (but tuple at def)
let! appliedArgText = text[appliedArgRange]
// only show param hint when at least one of the tuple params should be shown
if def |> Seq.exists (fun p -> ShouldCreate.paramHint func p appliedArgText) then
let defArgName =
let names = def |> Seq.map (fun p -> p.DisplayName) |> String.concat ","
"(" + names + ")"
let hint = createParamHint appliedArgRange defArgName
parameterHints.Add hint
| _ ->
// both tuple
for (p, eleRange) in Seq.zip def tupleRanges do
let! appliedArgText = text[eleRange]
if ShouldCreate.paramHint func p appliedArgText then
let defArgName = p.DisplayName
let hint = createParamHint eleRange defArgName
parameterHints.Add hint
| :? FSharpMemberOrFunctionOrValue as methodOrConstructor when
hintConfig.ShowParameterHints && methodOrConstructor.IsConstructor
-> // TODO: support methods when this API comes into FCS
let endPosForMethod = symbolUse.Range.End
let line, _ = Position.toZ endPosForMethod
let afterParenPosInLine =
getFirstPositionAfterParen (text.Lines.[line].ToString()) (endPosForMethod.Column)
let tupledParamInfos =
parseAndCheck.GetParseResults.FindParameterLocations(Position.fromZ line afterParenPosInLine)
let appliedArgRanges =
parseAndCheck.GetParseResults.GetAllArgumentsForFunctionApplicationAtPostion symbolUse.Range.Start
match tupledParamInfos, appliedArgRanges with
| None, None -> ()
// Prefer looking at the "tupled" view if it exists, even if the other ranges exist.
// M(1, 2) can give results for both, but in that case we want the "tupled" view.
| Some tupledParamInfos, _ ->
let parameters =
methodOrConstructor.CurriedParameterGroups |> Seq.concat |> Array.ofSeq // TODO: need ArgumentLocations to be surfaced
for idx = 0 to parameters.Length - 1 do
// let paramLocationInfo = tupledParamInfos.ArgumentLocations.[idx]
let param = parameters.[idx]
let paramName = param.DisplayName
// if shouldCreateHint param && paramLocationInfo.IsNamedArgument then
// let hint = { Text = paramName + " ="; Pos = paramLocationInfo.ArgumentRange.Start; Kind = Parameter }
// parameterHints.Add(hint)
()
// This will only happen for curried methods defined in F#.