-
Notifications
You must be signed in to change notification settings - Fork 2
/
servicem.fs
2115 lines (1844 loc) · 117 KB
/
servicem.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
// Copyright (c) Microsoft Open Technologies, Inc. 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.LanguageService
open Internal.Utilities.Collections
open Microsoft.FSharp.Compiler.SourceCodeServices
open System
open System.Text
open System.IO
open System.Collections.Generic
open System.Collections
open System.Configuration
open System.Diagnostics
open System.Globalization
open System.Threading
open System.ComponentModel.Design
open System.Runtime.InteropServices
open Microsoft.VisualStudio
open Microsoft.VisualStudio.FSharp.LanguageService
open Microsoft.VisualStudio.Shell
open Microsoft.VisualStudio.Shell.Interop
open Microsoft.VisualStudio.TextManager.Interop
open Microsoft.VisualStudio.Text
open Microsoft.VisualStudio.OLE.Interop
open Microsoft.VisualStudio.FSharp.LanguageService
open Microsoft.FSharp.Compiler.AbstractIL.Diagnostics
open Microsoft.FSharp.Compiler.AbstractIL.Internal.Library
open Microsoft.FSharp.Compiler.Lib
open Internal.Utilities.Debug
#nowarn "45" // This method will be made public in the underlying IL because it may implement an interface or override a method
#nowarn "47" // Self-referential uses within object constructors will be checked for initialization soundness at runtime. Consider placing self-references within 'do' statements after the last 'let' binding in a class
#if DEBUG
module ParserState =
open System.Text.RegularExpressions
let private rxState = Regex("state (\d+):", RegexOptions.Compiled)
let private (|StateLine|_|) l =
let m = rxState.Match l
if m.Success then m.Groups.[1].Value |> int |> Some
else None
let FsyListingVariableName = "FSY_LISTING"
let private cache =
lazy
match Environment.GetEnvironmentVariable FsyListingVariableName with
| null -> None
| x when not (File.Exists x) -> None
| path ->
let d = Dictionary()
try
(
use f = File.OpenText(path)
let rec doRead () =
match f.ReadLine() with
| null -> ()
| StateLine l ->
match readStates [] with
| Some states ->
d.Add(l, states)
doRead ()
| None -> ()
| _ -> doRead ()
and readStates acc =
match f.ReadLine() with
| null -> None
| x when String.IsNullOrEmpty x -> Some (List.rev acc)
| x -> readStates (x::acc)
doRead ()
)
let lastWriteTime = File.GetLastWriteTime(path)
Some (d, lastWriteTime, path)
with
_ -> None
let Get s =
match cache.Value with
| None -> None
| Some (dict, lastWriteTime, path) ->
match dict.TryGetValue s with
| true, v -> Some (v, lastWriteTime, path)
| _ -> None
#endif
module Implementation =
module FSharpConstants =
let fsharpCodeDomProviderName = "FSharp"
// These are the IDs from fslangservice.dll
let packageGuidString = "871D2A70-12A2-4e42-9440-425DD92A4116"
[<Literal>]
let languageServiceGuidString = Microsoft.VisualStudio.FSharp.Shared.FSharpCommonConstants.languageServiceGuidString
// These are the IDs from the Python sample:
let intellisenseProviderGuidString = "8b1807ea-d222-4765-afa8-c092d480e451"
// These are the entries from fslangservice.dll
let PLKMinEdition = "standard"
let PLKCompanyName = "Microsoft" // "Microsoft Corporation"
let PLKProductName = "f#" // "Visual Studio Integration of FSharp Language Service"
let PLKProductVersion = "1.0"
let PLKResourceID = 1s
// App.config keys for determining whether not-shipping features are turned on or off
// Edit devenv.exe.config and place these at the end.
// <appSettings>
// <add key="fsharp-regions-enabled" value="true" />
// <add key="fsharp-navigationbar-enabled" value="true" />
// <add key="fsharp-standalone-file-intellisense-enabled" value="false" />
// </appSettings>
let enableNavBar = "fsharp-navigationbar-enabled"
let enableRegions = "fsharp-regions-enabled"
let enableStandaloneFileIntellisense = "fsharp-standalone-file-intellisense-enabled"
let enableLanguageService = "fsharp-language-service-enabled"
type FSharpColorableItem(canonicalName: string, displayName : Lazy<string>, foreground, background) =
interface IVsColorableItem with
member x.GetDefaultColors(piForeground, piBackground) =
#if DEBUG
Check.ArrayArgumentNotNullOrEmpty piForeground "piForeground"
Check.ArrayArgumentNotNullOrEmpty piBackground "piBackground"
#endif
piForeground.[0] <- foreground
piBackground.[0] <- background
VSConstants.S_OK
member x.GetDefaultFontFlags(pdwFontFlags) =
pdwFontFlags <- 0u
VSConstants.S_OK
member x.GetDisplayName(pbstrName) =
pbstrName <- displayName.Force()
VSConstants.S_OK
interface IVsMergeableUIItem with
member this.GetCanonicalName(s) =
s <- canonicalName
VSConstants.S_OK
member this.GetDescription(s) =
s <- ""
VSConstants.S_OK
member x.GetDisplayName(s) =
s <- displayName.Force()
VSConstants.S_OK
member x.GetMergingPriority(i) =
i <- 0x1000 // as per docs, MS products should use a value between 0x1000 and 0x2000
VSConstants.S_OK
/// A Single declaration.
type FSharpDeclaration( documentationProvider : IdealDocumentationProvider,
decl:Declaration ) =
member d.Kind with get() = decl.Glyph // Note: Snippet is Kind=205
member d.Shortcut with get() = ""
member d.Title with get() = decl.Name
member d.Description
with get() =
XmlDocumentation.BuildDataTipText(documentationProvider,decl.DescriptionText)
type FSharpMethodListForAMethodTip(documentationProvider : IdealDocumentationProvider, methodsName, methods: Method[], nwpl : NoteworthyParamInfoLocations, snapshot : ITextSnapshot, isThisAStaticArgumentsTip : bool) =
inherit MethodListForAMethodTip()
let tupleEnds = [|
yield nwpl.LongIdStartLocation
yield nwpl.LongIdEndLocation
yield nwpl.OpenParenLocation
for i in 0..nwpl.TupleEndLocations.Length-2 do
let line,col = nwpl.TupleEndLocations.[i]
yield line, col-1 // col is the location of the comma, we want param to end just before it
let line, col = nwpl.TupleEndLocations.[nwpl.TupleEndLocations.Length-1]
yield line,(if nwpl.IsThereACloseParen then col-1 else col)
|]
let safe i dflt f = if 0 <= i && i < methods.Length then f methods.[i] else dflt
let parameterRanges =
let ss = snapshot
[|
// skip 2 because don't want longid start&end, just want open paren and tuple ends
for (sl,sc),(el,ec) in tupleEnds |> Seq.skip 2 |> Seq.pairwise do
let span = ss.CreateTrackingSpan(FSharpMethodListForAMethodTip.MakeSpan(ss,sl,sc,el,ec), SpanTrackingMode.EdgeInclusive)
yield span
|]
do assert(methods.Length > 0)
static member MakeSpan(ss:ITextSnapshot, sl, sc, el, ec) =
let makeSnapshotPoint l c =
let lineNum, fsharpRangeIsPastEOF =
// -1 because F# reports 1-based line nums, whereas VS wants 0-based
if l - 1 <= ss.LineCount - 1 then
l - 1, false
else
ss.LineCount - 1, true
let line = ss.GetLineFromLineNumber(lineNum)
line.Start.Add(if fsharpRangeIsPastEOF then line.Length else Math.Min(c, line.Length))
let start = makeSnapshotPoint sl sc
let end_ = makeSnapshotPoint el ec
assert(start.CompareTo(end_) <= 0)
(new SnapshotSpan(start, end_)).Span
override x.GetColumnOfStartOfLongId() = (snd nwpl.LongIdStartLocation)-1 // is 1-based, wants 0-based
override x.IsThereACloseParen() = nwpl.IsThereACloseParen
override x.GetNoteworthyParamInfoLocations() = tupleEnds
override x.GetParameterNames() = nwpl.NamedParamNames
override x.GetParameterRanges() = parameterRanges
override x.GetCount() = methods.Length
override x.GetDescription(index) =
safe index "" (fun m -> XmlDocumentation.BuildMethodOverloadTipText(documentationProvider, m.Description))
override x.GetType(index) = safe index "" (fun m -> m.Type)
override x.GetParameterCount(index) = safe index 0 (fun m -> m.Parameters.Length)
override x.GetParameterInfo(index, parameter, nameOut, displayOut, descriptionOut) =
let name,display,description = safe index ("","","") (fun m ->
let p = m.Parameters.[parameter]
p.Name,p.Display,p.Description )
nameOut <- name
displayOut <- display
descriptionOut <- description
override x.GetName(_index) = methodsName
override x.OpenBracket = if isThisAStaticArgumentsTip then "<" else "("
override x.CloseBracket = if isThisAStaticArgumentsTip then ">" else ")"
/// A collections of declarations as would be returned by a dot-completion request.
//
// Note, the Declarations type inherited by this code is defined in the F# Project System C# code. This is the only implementation
// in the codebase, hence we are free to change it and refactor things (e.g. bring more things into F# code)
// if we wish.
type FSharpDeclarations(declarations: FSharpDeclaration[], reason : BackgroundRequestReason) =
inherit Declarations()
// Sort the declarations, NOTE: we used ORDINAL comparison here, this is "by design" from F# 2.0, partly because it puts lowercase last.
let declarations = declarations |> Array.sortWith (fun d1 d2 -> compare d1.Title d2.Title)
let mutable lastBestMatch = ""
let isEmpty = (declarations.Length = 0)
let tab = Dictionary<string,FSharpDeclaration[]>()
// Given a prefix, narrow the items to the include the ones containing that prefix, and store in a lookaside table
// attached to this declaration set.
let trimmedDeclarations filterText : FSharpDeclaration[] =
if reason = BackgroundRequestReason.DisplayMemberList then declarations
elif tab.ContainsKey filterText then tab.[filterText]
else
let matcher = AbstractPatternMatcher.Singleton
let decls =
// Find the first prefix giving a non-empty declaration set after filtering
seq { for i in filterText.Length-1 .. -1 .. 0 do
let filterTextPrefix = filterText.[0..i]
match tab.TryGetValue filterTextPrefix with
| true, decls -> yield decls
| false, _ -> yield declarations |> Array.filter (fun s -> matcher.MatchSingleWordPattern(s.Title, filterTextPrefix)<>null)
yield declarations }
|> Seq.tryFind (fun arr -> arr.Length > 0)
|> (function None -> declarations | Some s -> s)
tab.[filterText] <- decls
decls
override decl.GetCount(filterText) =
let decls = trimmedDeclarations filterText
decls.Length
override decl.GetDisplayText(filterText, index) =
let decls = trimmedDeclarations filterText
if (index >= 0 && index < decls.Length) then
decls.[index].Title
else ""
override decl.IsEmpty() = isEmpty
override decl.GetName(filterText, index) =
let decls = trimmedDeclarations filterText
if (index >= 0 && index < decls.Length) then
let item = decls.[index]
if (item.Kind = 205) then
decls.[index].Shortcut
else
item.Title
else String.Empty
override decl.GetDescription(filterText, index) =
let decls = trimmedDeclarations filterText
if (index >= 0 && index < decls.Length) then
decls.[index].Description
else ""
override decl.GetGlyph(filterText, index) =
let decls = trimmedDeclarations filterText
//The following constants are the index of the various glyphs in the ressources of Microsoft.VisualStudio.Package.LanguageService.dll
if (index >= 0 && index < decls.Length) then
let item = decls.[index]
item.Kind
else 0
// This method is called to get the string to commit to the source buffer.
// Note that the initial extent is only what the user has typed so far.
override decl.OnCommit(filterText, index) =
// We intercept this call only to get the initial extent
// of what was committed to the source buffer.
let result = decl.GetName(filterText, index)
Microsoft.FSharp.Compiler.Lexhelp.Keywords.QuoteIdentifierIfNeeded result
override decl.IsCommitChar(commitCharacter) =
// Usual language identifier rules...
not (Char.IsLetterOrDigit(commitCharacter) || commitCharacter = '_')
// A helper to aid in determining how much text is relevant to the items chosen in the completion list.
override decl.Reason = reason
// Note, there is no real reason for this code to use byrefs, except that we're calling it from C#.
override decl.GetBestMatch(filterText, textSoFar, index : int byref, uniqueMatch : bool byref, shouldSelectItem : bool byref) =
let decls = trimmedDeclarations filterText
let compareStrings(s,t,l,b : bool) = System.String.Compare(s,0,t,0,l,b)
let tryFindDeclIndex text length ignoreCase =
decls
|> Array.tryFindIndex (fun d -> compareStrings(d.Title, text, length, ignoreCase) = 0)
// The best match is the first item that begins with the longest prefix of the
// given word (value).
let rec findMatchOfLength len ignoreCase =
if len = 0 then
let indexLastBestMatch = tryFindDeclIndex lastBestMatch lastBestMatch.Length ignoreCase
match indexLastBestMatch with
| Some index -> (index, false, false)
| None -> (0,false, false)
else
let firstMatchingLenChars = tryFindDeclIndex textSoFar len ignoreCase
match firstMatchingLenChars with
| Some index ->
lastBestMatch <- decls.[index].Title
let select = len = textSoFar.Length
if (index <> decls.Length- 1) && (compareStrings(decls.[index+1].Title , textSoFar, len, ignoreCase) = 0)
then (index, false, select)
else (index, select, select)
| None ->
match ignoreCase with
| false -> findMatchOfLength len true
| true -> findMatchOfLength (len-1) false
let (i, u, p) = findMatchOfLength textSoFar.Length false
index <- i
uniqueMatch <- u
let preselect =
// select an item in the list if what the user has typed is a prefix...
p || (
// ... or if the list has filtered down to a single item, and the user's text is still a 'match'
// for example, "System.Console.WrL" will filter down to one, and still be a match, whereas
// "System.Console.WrLx" will filter down to one, but no longer be a match
decls.Length = 1 &&
AbstractPatternMatcher.Singleton.MatchSingleWordPattern(decls.[0].Title, textSoFar)<>null
)
shouldSelectItem <- preselect
// This method is called after the string has been committed to the source buffer.
//
// Note: this override is a bit out of place as nothing in this type has anything to do with text buffers.
override decl.OnAutoComplete(_textView, _committedText, _commitCharacter, _index) =
// Would need special handling code for snippets.
'\000'
// ----------------------------------------------------------------------------------
// Provides functionality that is available based on the untyped AST
// This type ia a little complex.
// -- Each time we get a new UntypedParseInfo, we create a new UntypedFSharpScope that "folds in" the
// new region information while keeping the same unique identifiers for the regions in the text
//
// -- The navigation items in the object are computed lazily
type UntypedFSharpScope(untypedParse:UntypedParseInfo, prevRegions, regionGenerator) =
// Do we need to update the list?
let mutable navigationItems : NavigationItems option = None
let mutable displayedRegions = prevRegions
// Utilities
let copyTo (target:ArrayList) arr selector =
target.Clear()
for m in arr do
let (m:DeclarationItem) = selector m
let (sc, sl), (ec, el) = m.Range
let memb = new DropDownMember(m.Name, new TextSpan(iStartLine=sl - 1,iStartIndex=sc,iEndLine=el - 1,iEndIndex=ec),
m.Glyph, DROPDOWNFONTATTR.FONTATTR_PLAIN)
target.Add(memb) |> ignore
let findDeclaration (declarations:'a[]) allowEqualEndLine (selector:'a -> DeclarationItem) line _col =
let _, sel, _ =
declarations
|> Array.fold (fun (n, idx, size) decl ->
// TODO this looks like an algorithm that was ad-hoc'd to deal with bad ranges from the interactiveChecker, maybe can be simplified now
let (_, sl), (_, el) = (selector decl).Range
if ((line >= sl) && (line < el || (allowEqualEndLine && el = line))) && (el - sl) < size then
(n+1, n, el - sl)
else
(n+1, idx, size)
) (0, -1, Int32.MaxValue)
if sel<> -1 then sel else
let mutable lastBefore = -1
let mutable lastLine = -1
for i in 0 .. declarations.Length - 1 do
let decl = declarations.[i]
let (_, _sl), (_, el) = (selector decl).Range
if el < line && el > lastLine then
lastBefore <- i
lastLine <- el
if (lastBefore = -1 && declarations.Length > 0)
then 0 else lastBefore
let ensureNavigationItemsUpToDate() =
if navigationItems.IsNone then
navigationItems <- Some(untypedParse.GetNavigationItems())
member this.FileName = untypedParse.FileName
member this.Regions = displayedRegions
member this.RegionGenerator = regionGenerator
static member WithNewParseInfo(untypedParse:UntypedParseInfo, prev:UntypedFSharpScope option) =
match prev with
| Some(prev) ->
let regs =
if (prev.FileName = untypedParse.FileName) then
prev.Regions
else
Map.empty
new UntypedFSharpScope(untypedParse, regs, prev.RegionGenerator)
| None ->
let generator =
let count = ref 0u
(fun () -> count := !count + 1u; !count) // unchecked? overflow?
new UntypedFSharpScope(untypedParse, Map.empty, generator)
// Synchronize...
member this.SynchronizeNavigationDropDown(file, line, col:int, dropDownTypes:ArrayList, dropDownMembers:ArrayList, selectedType:int byref, selectedMember:int byref) =
#if DEBUG
use t = Trace.Call("LanguageService", "SynchronizeNavigationDropDown", fun _->sprintf " line=%d col=%d" line col)
#endif
try
let current = untypedParse.FileName
if file <> current then
dropDownTypes.Clear()
dropDownTypes.Add(new DropDownMember("(Parsing project files)", new TextSpan(), -1, DROPDOWNFONTATTR.FONTATTR_GRAY)) |> ignore
dropDownMembers.Clear()
selectedType <- 0
selectedMember <- -1
true
else
ensureNavigationItemsUpToDate ()
// Test whether things have changed so that we don't update the dropdown every time
copyTo dropDownTypes navigationItems.Value.Declarations (fun decl -> decl.Declaration)
let line = line + 1
let selLeft = findDeclaration navigationItems.Value.Declarations true (fun decl -> decl.Declaration) line col
selectedType <- selLeft
match selLeft with
| n when n >= 0 ->
copyTo dropDownMembers (navigationItems.Value.Declarations.[n].Nested) id
selectedMember <- findDeclaration navigationItems.Value.Declarations.[n].Nested true id line col
| _ ->
selectedMember <- -1
true
with e->
Assert.Exception(e)
reraise()
member x.ValidateBreakpointLocation(line,col) =
untypedParse.ValidateBreakpointLocation(line,col)
member x.GetHiddenRegions(file) =
ensureNavigationItemsUpToDate()
let current = untypedParse.FileName
match navigationItems with
| Some(res) when file = current ->
res.Declarations
|> Array.filter(fun decl -> not(decl.Declaration.IsSingleTopLevel))
|> Array.fold (fun (toCreate, toUpdate:Map<_,_>) decl ->
let declKey = decl.Declaration.UniqueName
let (sc, sl), (ec, el) = decl.Declaration.BodyRange
let context = new TextSpan(iEndIndex = ec, iEndLine = el-1, iStartIndex = sc, iStartLine = sl-1)
match (Map.tryFind declKey displayedRegions) with
| Some(uniqueId) ->
// do not add if the region hasn't changed
(toCreate, toUpdate.Add(uniqueId, context))
| None ->
let id = regionGenerator()
let reg =
new NewHiddenRegion
(iType = int HIDDEN_REGION_TYPE.hrtCollapsible, dwBehavior = uint32 HIDDEN_REGION_BEHAVIOR.hrbClientControlled,
dwState = uint32 HIDDEN_REGION_STATE.hrsExpanded, tsHiddenText = context, pszBanner = null, dwClient = id)
displayedRegions <- displayedRegions.Add(declKey, id)
(reg::toCreate, toUpdate)
) ([], Map.empty)
| _ ->
displayedRegions <- Map.empty
[], Map.empty
member x.ClearDisplayedRegions() =
displayedRegions <- Map.empty
/// The scope object is the result of computing a particular typecheck. It may be queried for things like
/// data tip text, member completion and so forth.
type FSharpScope(/// The recent result of parsing
untypedResults: UntypedParseInfo,
/// Line/column/snapshot of BackgroundRequest that initiated creation of this scope
brLine:int, brCol:int, brSnapshot:ITextSnapshot,
/// The possibly staler result of typechecking
typedResults: TypeCheckResults,
/// The project
projectSite: IProjectSite,
/// The text view
view: IVsTextView,
/// The colorizer for this view (though why do we need to be lazy about creating this?)
colorizer: Lazy<FSharpColorizer>,
/// A service that will provide Xml Content
documentationProvider : IdealDocumentationProvider
) =
inherit AuthoringScope()
// go ahead and compute this now, on this background thread, so will have info ready when UI thread asks
let noteworthyParamInfoLocations = untypedResults.FindNoteworthyParamInfoLocations(brLine, brCol)
let lastRequestedMethodListForMethodTip : MethodListForAMethodTip option ref = ref None
member scope.LastRequestedMethodListForMethodTipUsingFallback() =
lastRequestedMethodListForMethodTip := None
member scope.InitLastRequestedMethodListForMethodTipUsingFallback() =
lastRequestedMethodListForMethodTip := Some (scope.DoGetMethodListForAMethodTip(true))
static member HasTextChangedSinceLastTypecheck (curTextSnapshot: ITextSnapshot, oldTextSnapshot: ITextSnapshot, ((sl:int,sc:int),(el:int,ec:int))) =
// compare the text from (sl,sc) to (el,ec) to see if it changed from the old snapshot to the current one
// (sl,sc)-(el,ec) are line/col positions in the current snapshot
if el >= oldTextSnapshot.LineCount then
true // old did not even have 'el' many lines, note 'el' is zero-based
else
assert(el < curTextSnapshot.LineCount)
let oldFirstLine = oldTextSnapshot.GetLineFromLineNumber sl
let oldLastLine = oldTextSnapshot.GetLineFromLineNumber el
if oldFirstLine.Length < sc || oldLastLine.Length < ec then
true // one of old lines was not even long enough to contain the position we're looking at
else
let posOfStartInOld = oldFirstLine.Start.Position + sc
let posOfEndInOld = oldLastLine.Start.Position + ec
let curFirstLine = curTextSnapshot.GetLineFromLineNumber sl
let curLastLine = curTextSnapshot.GetLineFromLineNumber el
assert(curFirstLine.Length >= sc)
assert(curLastLine.Length >= ec)
let posOfStartInCur = curFirstLine.Start.Position + sc
let posOfEndInCur = curLastLine.Start.Position + ec
if posOfEndInCur - posOfStartInCur <> posOfEndInOld - posOfStartInOld then
true // length of text between two endpoints changed
else
let mutable oldPos = posOfStartInOld
let mutable curPos = posOfStartInCur
let mutable ok = true
while ok && oldPos < posOfEndInOld do
let oldChar = oldTextSnapshot.[oldPos]
let curChar = curTextSnapshot.[curPos]
if oldChar <> curChar then
ok <- false
oldPos <- oldPos + 1
curPos <- curPos + 1
not ok
member __.GetExtraColorizations() = typedResults.GetExtraColorizations()
override scope.GetDataTipText(line, col) =
// in cases like 'A<int>' when cursor in on '<' there is an ambiguity that cannot be resolved based only on lexer information
// '<' can be treated both as operator and as part of identifier
// in this case we'll do 2 passes:
// 1. treatTokenAsIdentifier=false - we'll pick raw token under the cursor and try find it among resolved names, is attempt was successful - great we are done, otherwise
// 2. treatTokenAsIdentifier=true - even if raw token was recognized as operator we'll use different branch
// that calls QuickParse.GetCompleteIdentifierIsland and then tries previous column...
let rec getDataTip(alwaysTreatTokenAsIdentifier) =
let token = colorizer.Value.GetTokenInfoAt(VsTextLines.TextColorState (VsTextView.Buffer view),line,col)
#if DEBUG
use t = Trace.Call("LanguageService",
"GetDataTipText",
fun _->sprintf " line=%d col=%d tokeninfo=%A" line col token.Token)
#endif
try
let lineText = VsTextLines.LineText (VsTextView.Buffer view) line
// If we're not on the first column; we don't find any identifier, we also look at the previous one
// This allows us to do Ctrl+K, I in this case: let f$ x = x
// Note: this is triggered by hovering over the next thing after 'f' as well - even in
// case like "f(x)" when hovering over "(", but MPF doesn't show tooltip in that case
// Note: MPF also doesn't show the tooltip if we're past the end of the line (Ctrl+K, I after
// the last character on the line), so tooltip isn't shown in that case (suggestion 4371)
// Try the actual column first...
let tokenTag, col, possibleIdentifier, makeSecondAttempt =
if token.Type = TokenType.Operator && not alwaysTreatTokenAsIdentifier then
let tag, startCol, endCol = OperatorToken.asIdentifier token
let op = lineText.Substring(startCol, endCol - startCol)
tag, startCol, Some(op, endCol, false), true
else
match (QuickParse.GetCompleteIdentifierIsland false lineText col) with
| None when col > 0 ->
// Try the previous column & get the token info for it
let tokenTag =
let token = colorizer.Value.GetTokenInfoAt(VsTextLines.TextColorState (VsTextView.Buffer view),line,col - 1)
token.Token
let possibleIdentifier = QuickParse.GetCompleteIdentifierIsland false lineText (col - 1)
tokenTag, col - 1, possibleIdentifier, false
| _ as poss -> token.Token, col, poss, false
#if DEBUG
let isDiagnostic = Keyboard.IsKeyPressed Keyboard.Keys.Shift
#else
let isDiagnostic = false
#endif
let diagnosticTipSpan = TextSpan(iStartLine=line, iEndLine=line, iStartIndex=col, iEndIndex=col+1)
match possibleIdentifier with
| None -> (if isDiagnostic then "No identifier found at this position." else ""),diagnosticTipSpan
| Some (s,colAtEndOfNames, isQuotedIdentifier) ->
// REVIEW: Need to capture and display XML
let diagnosticText lead =
let errorText = String.Concat(typedResults.Errors |> Seq.truncate 5 |> Seq.map(fun pi->sprintf "%s\n" pi.Message)|>Seq.toArray)
let errorText = match errorText.Length with 0->"" | _->"Errors:\n"+errorText
let dataTipText = sprintf "%s\nIsland(col=%d,token=%d):\n%A\n%s%s" lead col tokenTag possibleIdentifier (projectSite.DescriptionOfProject()) errorText
dataTipText
if typedResults.HasFullTypeCheckInfo then
let qualId = PrettyNaming.GetLongNameFromString s
#if DEBUG
Trace.PrintLine("LanguageService", (fun () -> sprintf "Got qualId = %A" qualId))
#endif
let parserState =
#if DEBUG
match qualId with
| [x] ->
match Int32.TryParse x with
| true, v ->
match ParserState.Get v with
| Some (lines, lastWriteTime, path) ->
Some [ yield sprintf "Listing file: %s" path
yield sprintf "Last write time: %A" lastWriteTime
yield! lines ]
| None ->
Some [ "Grammar debugging requires FSYacc listing file"
"FSYacc puts listing in the same folder with output file but with extension 'fsyacc.output'."
"If output file is not set - then listing will be placed in the folder with input file and will use the name of input file (with extension fsyacc.output)"
"run 'fsyacc -v <input-file>"
sprintf "Create %s env variable with value - path to the listing file" ParserState.FsyListingVariableName ]
| _ -> None
| _ -> None
#else
None
#endif
// Corrrect the identifier (e.g. to correctly handle active pattern names that end with "BAR" token)
let tokenTag = QuickParse.CorrectIdentifierToken s tokenTag
let dataTip = typedResults.GetDataTipText((line, colAtEndOfNames), lineText, qualId, tokenTag)
#if DEBUG
Trace.PrintLine("LanguageService", fun () -> sprintf "Got datatip=%A" dataTip)
#endif
match dataTip with
| DataTipText [] when makeSecondAttempt -> getDataTip true
| _ ->
if isDiagnostic then
let text = sprintf "plid:%A\ndataTip:\n%A" qualId dataTip
let text =
match parserState with
| None -> text
| Some lines ->
sprintf "%s\n%s\n" text (String.concat "\n" lines)
diagnosticText text, diagnosticTipSpan
else
let dataTipText = XmlDocumentation.BuildDataTipText(documentationProvider, dataTip)
// The data tip is located w.r.t. the start of the last identifier
let sizeFixup = if isQuotedIdentifier then 4 else 0
let lastStringLength = (qualId |> List.rev |> List.head).Length + sizeFixup
#if DEBUG
Trace.PrintLine("LanguageService", (fun () -> sprintf "Got dataTip = %A, colOfEndOfText = %d, lastStringLength = %d, line = %d" dataTipText colAtEndOfNames lastStringLength line))
#endif
// This is the span of text over which the data tip is active. If the mouse moves away from it then the
// data tip goes away
let dataTipSpan = TextSpan(iStartLine=line, iEndLine=line, iStartIndex=max 0 (colAtEndOfNames-lastStringLength), iEndIndex=colAtEndOfNames)
(dataTipText, dataTipSpan)
else
"Bug: TypeCheckInfo option was None", diagnosticTipSpan
with e->
Assert.Exception(e)
reraise()
getDataTip false
static member IsReasonRequiringSyncParse(reason) =
match reason with
| BackgroundRequestReason.MethodTip // param info...
| BackgroundRequestReason.MatchBracesAndMethodTip // param info...
| BackgroundRequestReason.CompleteWord | BackgroundRequestReason.MemberSelect | BackgroundRequestReason.DisplayMemberList // and intellisense-completion...
-> true // ...require a sync parse (so as to call FindNoteworthyParamInfoLocations and GetRangeOfExprLeftOfDot, respectively)
| _ -> false
/// Intellisense autocompletions
[<CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2233:OperationsShouldNotOverflow", MessageId="col-1")>]
override scope.GetDeclarations(textSnapshot, line, col, reason) =
ignore () // to be able to place a breakpoint
assert(FSharpScope.IsReasonRequiringSyncParse(reason))
async {
let tokenInfo = colorizer.Value.GetTokenInfoAt(VsTextLines.TextColorState (VsTextView.Buffer view),line,col)
let prevCol = if (col <= 0) then 0 else col - 1 // Note: check <= 0 to show FxCop that there is no underflow risk
let prevTokenInfo = colorizer.Value.GetTokenInfoAt(VsTextLines.TextColorState (VsTextView.Buffer view),line,prevCol)
// denotes if we got token that matches exact specified position or it was just last token before EOF
let exactMatch = col >= tokenInfo.StartIndex && col <= tokenInfo.EndIndex
#if DEBUG
use _t = Trace.Call("LanguageService",
"GetDeclarations",
fun _->sprintf " line=%d col=%d reason=%A" line col reason)
#endif
try
if exactMatch &&
(
(tokenInfo.Color = TokenColor.Comment && prevTokenInfo.Color = TokenColor.Comment) ||
(tokenInfo.Color = TokenColor.String && prevTokenInfo.Color = TokenColor.String)
) then
// We don't want to show info in comments & strings (in case of exact match)
// (but we want to show it if the thing before or after isn't comment/string)
dprintf "GetDeclarations: We won't show anything in comment or string.\n"
return null
elif typedResults.HasFullTypeCheckInfo then
let lineText = VsTextLines.LineText (VsTextView.Buffer view) line
let colorState = VsTextLines.TextColorState (VsTextView.Buffer view)
let state = VsTextColorState.GetColorStateAtStartOfLine colorState line
let tokens = colorizer.Value.GetFullLineInfo(lineText, state)
// An ugly check to suppress declaration lists at 'System.Int32.'
if reason = BackgroundRequestReason.MemberSelect && col > 1 && lineText.[col-2]='.' then
// System.Int32..Parse("42")
// just pressed dot here ^
// don't want any completion for that, we only trigger a MemberSelect on the ".." token in order to be able to get completion
// System.Int32..Parse("42")
// here ^
return null
// An ugly check to suppress declaration lists at 'member' declarations
elif QuickParse.TestMemberOrOverrideDeclaration tokens then
return null
else
let untypedParseInfoOpt =
if reason = BackgroundRequestReason.MemberSelect || reason = BackgroundRequestReason.DisplayMemberList || reason = BackgroundRequestReason.CompleteWord then
Some untypedResults
else
None
// TODO don't use QuickParse below, we have parse info available
let plid = QuickParse.GetPartialLongNameEx(lineText, col-1) // Subtract one to convert to zero-relative
ignore plid // for breakpoint
let detectTextChange (oldTextSnapshotInfo: obj, range) =
let oldTextSnapshot = oldTextSnapshotInfo :?> ITextSnapshot
FSharpScope.HasTextChangedSinceLastTypecheck (textSnapshot, oldTextSnapshot, range)
let! decls = typedResults.GetDeclarations(untypedParseInfoOpt,(line, col), lineText, plid, detectTextChange)
let declarations = decls.Items |> Array.map (fun d -> FSharpDeclaration(documentationProvider, d))
return (new FSharpDeclarations(declarations, reason) :> Declarations)
else
dprintf "GetDeclarations found no TypeCheckInfo in ParseResult.\n"
return null
with e->
Assert.Exception(e)
raise e
return null
}
/// Get methods for parameter info
override scope.GetMethodListForAMethodTip(useNameResolutionFallback) =
if useNameResolutionFallback then
// lastRequestedMethodListForMethodTip should be initialized in the ExecuteBackgroundRequest
// if not - just return null
defaultArg lastRequestedMethodListForMethodTip.Value null
else
// false is passed only from unit tests
scope.DoGetMethodListForAMethodTip(false)
member scope.DoGetMethodListForAMethodTip(useNameResolutionFallback) =
#if DEBUG
use t = Trace.Call("LanguageService",
"GetMethodListForAMethodTip",
fun _->sprintf " line=%d col=%d" brLine brCol)
#endif
try
// we need some typecheck info, even if stale, in order to look up e.g. method overload types/xmldocs
if typedResults.HasFullTypeCheckInfo then
// we need recent parse info to e.g. know how many commas and thus how many args there are
match noteworthyParamInfoLocations with
| Some nwpl ->
// Note: this may alternatively workaround some parts of 90778 - the real fix for that is to have before-overload-resolution name-sink work correctly.
// However it also deals with stale typecheck info that may not have recorded name resolutions for a recently-typed long-id.
let names =
if not useNameResolutionFallback then
None
else
Some nwpl.LongId
// "names" is a long-id that we can fallback-lookup in the local environment if captured name resolutions finds nothing at the location.
// This can happen e.g. if you are typing quickly and the typecheck results are stale enough that you don't have a captured resolution for
// the name you just typed, but fresh enough that you do have the right name-resolution-environment to look up the name.
let lidEndLine,lidEndCol = nwpl.LongIdEndLocation
let methods = typedResults.GetMethods((lidEndLine-1, lidEndCol-1), "", names) // -1 because wants 0-based
// If the name is an operator ending with ">" then it is a mistake
// we can't tell whether " >(" is a generic method call or an operator use
// (it depends on the previous line), so we fitler it
//
// Note: this test isn't particularly elegant - encoded operator name would be something like "( ...> )"
if (methods.Methods.Length = 0 || methods.Name.EndsWith("> )")) then
null
else
// "methods" contains both real methods for this longId, as well as static-parameters in the case of type providers.
// They "conflict" for cases of TP(...) (calling a constructor, no static args provided) versus TP<...> (static args), since
// both point to the same longId. However we can look at the character at the 'OpenParen' location and see if it is a '(' or a '<' and then
// filter the "methods" list accordingly.
let isThisAStaticArgumentsTip =
let parenLine, parenCol = nwpl.OpenParenLocation
let textAtOpenParenLocation =
if brSnapshot=null then
// we are unit testing, use the view
let _hr, buf = view.GetBuffer()
let _hr, s = buf.GetLineText(parenLine-1, parenCol-1, parenLine-1, parenCol) // -1 because F# reports 1-based line nums, whereas VS wants 0-based
s
else
// we are in the product, use the ITextSnapshot
brSnapshot.GetText(FSharpMethodListForAMethodTip.MakeSpan(brSnapshot, parenLine, parenCol-1, parenLine, parenCol))
if textAtOpenParenLocation = "<" then
true
else
false // note: textAtOpenParenLocation is not necessarily otherwise "(", for example in "sin 42.0" it is "4"
let filteredMethods =
[| for m in methods.Methods do
if m.IsStaticArguments = isThisAStaticArgumentsTip then // need to distinguish TP<...>(...) angle brackets tip from parens tip
yield m |]
if filteredMethods.Length <> 0 then
FSharpMethodListForAMethodTip(documentationProvider, methods.Name, filteredMethods, nwpl, brSnapshot, isThisAStaticArgumentsTip) :> MethodListForAMethodTip
else
null
| _ ->
null
else
dprintf "GetMethodListForAMethodTip found no TypeCheckInfo in ParseResult.\n"
null
with e->
Assert.Exception(e)
reraise()
override this.GetF1KeywordString(span : TextSpan, context : IVsUserContext) : unit =
let shouldTryToFindIdentToTheLeft (token : TokenInformation) =
match token.CharClass with
| TokenCharKind.WhiteSpace -> true
| TokenCharKind.Delimiter -> true
| _ -> false
let keyword =
let line = span.iStartLine
let lineText = VsTextLines.LineText (VsTextView.Buffer view) line
let tokenInformation, col =
let col =
if span.iStartIndex = lineText.Length && span.iStartIndex > 0 then
// if we are at the end of the line, we always step back one character
span.iStartIndex - 1
else
span.iStartIndex
let textColorState = VsTextLines.TextColorState (VsTextView.Buffer view)
match colorizer.Value.GetTokenInformationAt(textColorState,line,col) with
| (Some token) as original when col > 0 && shouldTryToFindIdentToTheLeft token ->
// try to step back one char
match colorizer.Value.GetTokenInformationAt(textColorState,line,col-1) with
| (Some token) as newInfo when token.CharClass <> TokenCharKind.WhiteSpace -> newInfo, col - 1
| _ -> original, col
| otherwise -> otherwise, col
match tokenInformation with
| None -> None
| Some token ->
match token.CharClass, token.ColorClass with
| TokenCharKind.Keyword, _
| TokenCharKind.Operator, _
| _, TokenColorKind.PreprocessorKeyword ->
lineText.Substring(token.LeftColumn, token.RightColumn - token.LeftColumn + 1) + "_FS" |> Some
| (TokenCharKind.Comment|TokenCharKind.LineComment), _ -> Some "comment_FS"
| TokenCharKind.Identifier, _ ->
try
let lineText = VsTextLines.LineText (VsTextView.Buffer view) line
let possibleIdentifier = QuickParse.GetCompleteIdentifierIsland false lineText col
match possibleIdentifier with
| None -> None // no help keyword
| Some(s,colAtEndOfNames, _) ->
if typedResults.HasFullTypeCheckInfo then
let qualId = PrettyNaming.GetLongNameFromString s
match typedResults.GetF1Keyword((line,colAtEndOfNames), lineText, qualId) with
| Some s -> Some s
| None -> None
else None
with e ->
Assert.Exception (e)
reraise()
| _ -> None
match keyword with
| Some f1Keyword ->
context.AddAttribute(VSUSERCONTEXTATTRIBUTEUSAGE.VSUC_Usage_Filter, "devlang", "fsharp") |> ignore
// TargetFrameworkMoniker is not set for files that are not part of project (scripts and orphan fs files)
if not (String.IsNullOrEmpty projectSite.TargetFrameworkMoniker) then
context.AddAttribute(VSUSERCONTEXTATTRIBUTEUSAGE.VSUC_Usage_Filter, "TargetFrameworkMoniker", projectSite.TargetFrameworkMoniker) |> ignore
context.AddAttribute(VSUSERCONTEXTATTRIBUTEUSAGE.VSUC_Usage_LookupF1_CaseSensitive, "keyword", f1Keyword) |> ignore
()
| None -> ()
// for tests
member this.GotoDefinition (textView, line, column) =
GotoDefinition.GotoDefinition (colorizer.Value, typedResults, textView, line, column)
override this.Goto (textView, line, column) =
GotoDefinition.GotoDefinition (colorizer.Value, typedResults, textView, line, column)
open Implementation
/// This class defines capabilities of the language service.
/// CodeSense = true\false, for example
type FSharpLanguagePreferences(site, langSvc, name) =
inherit LanguagePreferences(site, langSvc, name)
type ExecuteBackgroundRequestData =
{
projectSite : IProjectSite
checkOptions : CheckOptions
projectFileName : string
interactiveChecker : InteractiveChecker
colorizer : Lazy<FSharpColorizer>
}
type FSharpBackgroundRequest(line, col, info, sourceText, snapshot : ITextSnapshot,
methodTipMiscellany : MethodTipMiscellany, fileName, reason, view, sink,
source:ISource, timestamp:int, synchronous:bool,
executeBackgroundRequestData : Lazy<ExecuteBackgroundRequestData> option) =
inherit BackgroundRequest(line, col, info, sourceText, snapshot, methodTipMiscellany, fileName, reason, view, sink, source, timestamp, synchronous)
member this.ExecuteBackgroundRequestData = executeBackgroundRequestData
member this.TryGetColorizer() = match executeBackgroundRequestData with None -> None | Some data -> Some (data.Force().colorizer.Force())
// Container class that delays loading of FSharp.Compiler.dll compiler types until they're actually needed
type internal InteractiveCheckerContainer(interactiveChecker) =
member this.InteractiveChecker = interactiveChecker
/// LanguageService state.
//
// Note: It appears that a load of this type (+ a construction of an instance) should not load FSharp.Compiler.dll. This is subtle
// because it depends on deferred loading characteristics of the CLR. The type InteractiveCheckerContainer is an (otherwise
// unnecessary) indirection holding the types referenced in FSharp.Compiler.dll. This is sufficient to delay loading.
type internal LanguageServiceState() =
static let colorizerGuid = new Guid("{A2976312-7D71-4BB4-A5F8-66A08EBF46C8}") // Guid for colorizwed user data on IVsTextBuffer
let mutable serviceProvider:ServiceProvider option = None
let mutable interactiveCheckerContainerOpt:InteractiveCheckerContainer option = None
let mutable artifacts:Artifacts option = None
let mutable preferences:LanguagePreferences option = None
let mutable documentationProvider:IdealDocumentationProvider option = None
let mutable sourceFactory : (IVsTextLines -> IdealSource) option = None
let mutable dirtyForTypeCheckFiles : Set<string> = Set.empty
let mutable isInitialized = false
let mutable unhooked = false
let mutable untypedParseScope : UntypedFSharpScope option = None
let mutable enableStandaloneFileIntellisense = true
let outOfDateProjectFileNames = new System.Collections.Generic.HashSet<string>()
member this.InteractiveChecker =
if not this.IsInitialized then raise (Error.UseOfUninitializedLanguageServiceState)
match interactiveCheckerContainerOpt with
| Some interactiveCheckerContainer -> interactiveCheckerContainer.InteractiveChecker
| None ->
let notifyFileTypeCheckStateIsDirty = NotifyFileTypeCheckStateIsDirty (fun filename -> UIThread.Run(fun () -> this.NotifyFileTypeCheckStateIsDirty(filename)))
let interactiveChecker = InteractiveChecker.Create(notifyFileTypeCheckStateIsDirty)
let pc = InteractiveCheckerContainer(interactiveChecker)
interactiveCheckerContainerOpt <- Some pc
interactiveChecker
member this.Artifacts =
if not this.IsInitialized then raise (Error.UseOfUninitializedLanguageServiceState)
match artifacts with
| Some artifacts -> artifacts
| None->
let a = new Artifacts()
artifacts <- Some a