forked from ionide/FsAutoComplete
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Commands.fs
2299 lines (1914 loc) · 85.5 KB
/
Commands.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
namespace FsAutoComplete
open System
open System.IO
open Fantomas.Client.Contracts
open Fantomas.Client.LSPFantomasService
open FsAutoComplete.Logging
open FsAutoComplete.UnionPatternMatchCaseGenerator
open FsAutoComplete.RecordStubGenerator
open System.Threading
open Utils
open FSharp.Compiler.CodeAnalysis
open FSharp.Compiler.Diagnostics
open FSharp.Compiler.EditorServices
open FSharp.Compiler.Text
open Ionide.ProjInfo
open Ionide.ProjInfo.ProjectSystem
open FsToolkit.ErrorHandling
open FSharp.Analyzers
open FSharp.UMX
open FSharp.Compiler.Tokenization
open SymbolLocation
open FSharp.Compiler.Symbols
open System.Collections.Immutable
open System.Collections.Generic
open Ionide.ProjInfo.ProjectSystem
open FSharp.Compiler.Syntax
[<RequireQualifiedAccess>]
type LocationResponse<'a> = Use of 'a
[<RequireQualifiedAccess>]
type HelpText =
| Simple of symbol: string * text: string
| Full of symbol: string * tip: ToolTipText * textEdits: CompletionNamespaceInsert option
[<RequireQualifiedAccess>]
type CoreResponse<'a> =
| InfoRes of text: string
| ErrorRes of text: string
| Res of 'a
[<RequireQualifiedAccess>]
type FormatDocumentResponse =
| Formatted of source: NamedText * formatted: string
| FormattedRange of source: NamedText * formatted: string * range: FormatSelectionRange
| UnChanged
| Ignored
| ToolNotPresent
| Error of string
/// Represents a desired change to a given file
type DocumentEdit =
{ InsertPosition: pos
InsertText: string }
module private Result =
let ofCoreResponse (r: CoreResponse<'a>) =
match r with
| CoreResponse.Res a -> Ok a
| CoreResponse.ErrorRes msg
| CoreResponse.InfoRes msg -> Error msg
module AsyncResult =
let inline mapErrorRes ar : Async<CoreResponse<'a>> =
AsyncResult.foldResult id CoreResponse.ErrorRes ar
let recoverCancellationGeneric (ar: Async<Result<'t, exn>>) recoverInternal =
AsyncResult.foldResult id recoverInternal ar
let recoverCancellation (ar: Async<Result<CoreResponse<'t>, exn>>) =
recoverCancellationGeneric ar (sprintf "Request cancelled (exn was %A)" >> CoreResponse.InfoRes)
let recoverCancellationIgnore (ar: Async<Result<unit, exn>>) = AsyncResult.foldResult id ignore ar
[<RequireQualifiedAccess>]
type NotificationEvent =
| ParseError of errors: FSharpDiagnostic[] * file: string<LocalPath>
| Workspace of ProjectSystem.ProjectResponse
| AnalyzerMessage of messages: FSharp.Analyzers.SDK.Message[] * file: string<LocalPath>
| UnusedOpens of file: string<LocalPath> * opens: Range[]
// | Lint of file: string<LocalPath> * warningsWithCodes: Lint.EnrichedLintWarning list
| UnusedDeclarations of file: string<LocalPath> * decls: range[]
| SimplifyNames of file: string<LocalPath> * names: SimplifyNames.SimplifiableRange[]
| Canceled of errorMessage: string
| FileParsed of string<LocalPath>
| TestDetected of file: string<LocalPath> * tests: TestAdapter.TestAdapterEntry<range>[]
module Commands =
let fantomasLogger = LogProvider.getLoggerByName "Fantomas"
let commandsLogger = LogProvider.getLoggerByName "Commands"
let addFile (fsprojPath: string) fileVirtPath =
async {
try
let dir = Path.GetDirectoryName fsprojPath
let newFilePath = Path.Combine(dir, fileVirtPath)
let fileInfo = FileInfo(newFilePath)
// Ensure the destination directory exist
if not fileInfo.Directory.Exists then
fileInfo.Directory.Create()
(File.Open(newFilePath, FileMode.OpenOrCreate)).Close()
FsProjEditor.addFile fsprojPath fileVirtPath
return CoreResponse.Res()
with ex ->
return CoreResponse.ErrorRes ex.Message
}
let addFileAbove (fsprojPath: string) (fileVirtPath: string) newFileName =
async {
try
let dir = Path.GetDirectoryName fsprojPath
let virtPathDir = Path.GetDirectoryName fileVirtPath
let newFilePath = Path.Combine(dir, virtPathDir, newFileName)
let fileInfo = FileInfo(newFilePath)
// Ensure the destination directory exist
if not fileInfo.Directory.Exists then
fileInfo.Directory.Create()
(File.Open(newFilePath, FileMode.OpenOrCreate)).Close()
let newVirtPath = Path.Combine(virtPathDir, newFileName)
FsProjEditor.addFileAbove fsprojPath fileVirtPath newVirtPath
return CoreResponse.Res()
with ex ->
return CoreResponse.ErrorRes ex.Message
}
let addFileBelow (fsprojPath: string) (fileVirtPath: string) newFileName =
async {
try
let dir = Path.GetDirectoryName fsprojPath
let virtPathDir = Path.GetDirectoryName fileVirtPath
let newFilePath = Path.Combine(dir, virtPathDir, newFileName)
let fileInfo = FileInfo(newFilePath)
// Ensure the destination directory exist
if not fileInfo.Directory.Exists then
fileInfo.Directory.Create()
(File.Open(newFilePath, FileMode.OpenOrCreate)).Close()
let newVirtPath = Path.Combine(virtPathDir, newFileName)
FsProjEditor.addFileBelow fsprojPath fileVirtPath newVirtPath
return CoreResponse.Res()
with ex ->
return CoreResponse.ErrorRes ex.Message
}
let removeFile fsprojPath fileVirtPath =
async {
FsProjEditor.removeFile fsprojPath fileVirtPath
return CoreResponse.Res()
}
let addExistingFile fsprojPath fileVirtPath =
async {
FsProjEditor.addExistingFile fsprojPath fileVirtPath
return CoreResponse.Res()
}
let getRangesAtPosition (getParseResultsForFile: _ -> Async<Result<FSharpParseFileResults, _>>) file positions =
asyncResult {
let! ast = getParseResultsForFile file
return positions |> List.map (UntypedAstUtils.getRangesAtPosition ast.ParseTree)
}
let scopesForFile
(getParseResultsForFile: _ -> Async<Result<NamedText * FSharpParseFileResults, _>>)
(file: string<LocalPath>)
=
asyncResult {
let! (text, ast) = getParseResultsForFile file
let ranges =
Structure.getOutliningRanges (text.ToString().Split("\n")) ast.ParseTree
return ranges
}
let docForText (lines: NamedText) (tyRes: ParseAndCheckResults) : Document =
{ LineCount = lines.Lines.Length
FullName = tyRes.FileName // from the compiler, assumed safe
GetText = fun _ -> string lines
GetLineText0 = fun i -> (lines :> ISourceText).GetLineString i
GetLineText1 = fun i -> (lines :> ISourceText).GetLineString(i - 1) }
let getAbstractClassStub
tryFindAbstractClassExprInBufferAtPos
writeAbstractClassStub
(tyRes: ParseAndCheckResults)
(objExprRange: Range)
(lines: NamedText)
(lineStr: LineStr)
=
asyncResult {
let doc = docForText lines tyRes
let! abstractClass =
tryFindAbstractClassExprInBufferAtPos objExprRange.Start doc
|> Async.map (Result.ofOption (fun _ -> CoreResponse.InfoRes "Abstract class at position not found"))
let! (insertPosition, generatedCode) =
writeAbstractClassStub tyRes doc lines lineStr abstractClass
|> Async.map (Result.ofOption (fun _ -> CoreResponse.InfoRes "Didn't need to write an abstract class"))
return CoreResponse.Res(generatedCode, insertPosition)
}
let getRecordStub
tryFindRecordDefinitionFromPos
(tyRes: ParseAndCheckResults)
(pos: Position)
(lines: NamedText)
(line: LineStr)
=
async {
let doc = docForText lines tyRes
let! res = tryFindRecordDefinitionFromPos pos doc
match res with
| None -> return CoreResponse.InfoRes "Record at position not found"
| Some (recordEpr, (Some recordDefinition), insertionPos) ->
if shouldGenerateRecordStub recordEpr recordDefinition then
let result = formatRecord insertionPos "$1" recordDefinition recordEpr.FieldExprList
return CoreResponse.Res(result, insertionPos.InsertionPos)
else
return CoreResponse.InfoRes "Record at position not found"
| _ -> return CoreResponse.InfoRes "Record at position not found"
}
let getNamespaceSuggestions (tyRes: ParseAndCheckResults) (pos: Position) (line: LineStr) =
async {
match Lexer.findLongIdents (pos.Column, line) with
| None -> return CoreResponse.InfoRes "Ident not found"
| Some (_, idents) ->
match ParsedInput.GetEntityKind(pos, tyRes.GetParseResults.ParseTree) with
| None -> return CoreResponse.InfoRes "EntityKind not found"
| Some entityKind ->
let symbol = Lexer.getSymbol pos.Line pos.Column line SymbolLookupKind.Fuzzy [||]
match symbol with
| None -> return CoreResponse.InfoRes "Symbol at position not found"
| Some sym ->
let entities = tyRes.GetAllEntities true
let isAttribute = entityKind = EntityKind.Attribute
let entities =
entities
|> List.filter (fun e ->
match entityKind, (e.Kind LookupType.Fuzzy) with
| EntityKind.Attribute, EntityKind.Attribute
| EntityKind.Type, (EntityKind.Type | EntityKind.Attribute)
| EntityKind.FunctionOrValue _, _ -> true
| EntityKind.Attribute, _
| _, EntityKind.Module _
| EntityKind.Module _, _
| EntityKind.Type, _ -> false)
let maybeUnresolvedIdents =
idents |> Array.map (fun ident -> { Ident = ident; Resolved = false })
let entities =
entities
|> List.collect (fun e ->
[ yield e.TopRequireQualifiedAccessParent, e.AutoOpenParent, e.Namespace, e.CleanedIdents
if isAttribute then
let lastIdent = e.CleanedIdents.[e.CleanedIdents.Length - 1]
if
(e.Kind LookupType.Fuzzy) = EntityKind.Attribute
&& lastIdent.EndsWith "Attribute"
then
yield
e.TopRequireQualifiedAccessParent,
e.AutoOpenParent,
e.Namespace,
e.CleanedIdents
|> Array.replace (e.CleanedIdents.Length - 1) (lastIdent.Substring(0, lastIdent.Length - 9)) ])
let createEntity =
ParsedInput.TryFindInsertionContext
pos.Line
tyRes.GetParseResults.ParseTree
maybeUnresolvedIdents
OpenStatementInsertionPoint.Nearest
let word = sym.Text
let candidates = entities |> Seq.collect createEntity |> Seq.toList
let openNamespace =
candidates
|> List.choose (fun (entity, ctx) ->
entity.Namespace |> Option.map (fun ns -> ns, entity.FullDisplayName, ctx))
|> List.groupBy (fun (ns, _, _) -> ns)
|> List.map (fun (ns, xs) ->
ns,
xs
|> List.map (fun (_, name, ctx) -> name, ctx)
|> List.distinctBy (fun (name, _) -> name)
|> List.sortBy fst)
|> List.collect (fun (ns, names) ->
let multipleNames =
match names with
| [] -> false
| [ _ ] -> false
| _ -> true
names |> List.map (fun (name, ctx) -> ns, name, ctx, multipleNames))
let qualifySymbolActions =
candidates
|> List.map (fun (entity, _) -> entity.FullRelativeName, entity.Qualifier)
|> List.distinct
|> List.sort
return CoreResponse.Res(word, openNamespace, qualifySymbolActions)
}
let getUnionPatternMatchCases
tryFindUnionDefinitionFromPos
(tyRes: ParseAndCheckResults)
(pos: Position)
(lines: ISourceText)
(line: LineStr)
=
async {
let doc =
{ Document.LineCount = lines.Length
FullName = tyRes.FileName
GetText = fun _ -> string lines
GetLineText0 = fun i -> lines.GetLineString i
GetLineText1 = fun i -> lines.GetLineString(i - 1) }
let! res = tryFindUnionDefinitionFromPos pos doc
match res with
| None -> return CoreResponse.InfoRes "Union at position not found"
| Some (patMatchExpr, unionTypeDefinition, insertionPos) ->
if shouldGenerateUnionPatternMatchCases patMatchExpr unionTypeDefinition then
let result = formatMatchExpr insertionPos "$1" patMatchExpr unionTypeDefinition
return CoreResponse.Res(result, insertionPos.InsertionPos)
else
return CoreResponse.InfoRes "Union at position not found"
}
let formatSelection
(tryGetFileCheckerOptionsWithLines: _ -> Result<NamedText, _>)
(formatSelectionAsync: _ -> System.Threading.Tasks.Task<FantomasResponse>)
(file: string<LocalPath>)
(rangeToFormat: FormatSelectionRange)
: Async<Result<FormatDocumentResponse, string>> =
asyncResult {
try
let filePath = (UMX.untag file)
let! text = tryGetFileCheckerOptionsWithLines file
let currentCode = string text
let! fantomasResponse =
formatSelectionAsync
{ SourceCode = currentCode
FilePath = filePath
Config = None
Range = rangeToFormat }
match fantomasResponse with
| { Code = 1
Content = Some code
SelectedRange = Some range } ->
fantomasLogger.debug (Log.setMessage (sprintf "Fantomas daemon was able to format selection in \"%A\"" file))
return FormatDocumentResponse.FormattedRange(text, code, range)
| { Code = 2 } ->
fantomasLogger.debug (Log.setMessage (sprintf "\"%A\" did not change after formatting" file))
return FormatDocumentResponse.UnChanged
| { Code = 3; Content = Some error } ->
fantomasLogger.error (Log.setMessage (sprintf "Error while formatting \"%A\"\n%s" file error))
return FormatDocumentResponse.Error(sprintf "Formatting failed!\n%A" fantomasResponse)
| { Code = 4 } ->
fantomasLogger.debug (Log.setMessage (sprintf "\"%A\" was listed in a .fantomasignore file" file))
return FormatDocumentResponse.Ignored
| { Code = 6 } -> return FormatDocumentResponse.ToolNotPresent
| _ ->
fantomasLogger.warn (
Log.setMessage (
sprintf
"Fantomas daemon was unable to format \"%A\", due to unexpected result code %i\n%A"
file
fantomasResponse.Code
fantomasResponse
)
)
return FormatDocumentResponse.Error(sprintf "Formatting failed!\n%A" fantomasResponse)
with ex ->
fantomasLogger.warn (
Log.setMessage "Errors while formatting file, defaulting to previous content. Error message was {message}"
>> Log.addContextDestructured "message" ex.Message
>> Log.addExn ex
)
return! Core.Error ex.Message
}
let formatDocument
(tryGetFileCheckerOptionsWithLines: _ -> Result<NamedText, _>)
(formatDocumentAsync: _ -> System.Threading.Tasks.Task<FantomasResponse>)
(file: string<LocalPath>)
: Async<Result<FormatDocumentResponse, string>> =
asyncResult {
try
let filePath = (UMX.untag file)
let! text = tryGetFileCheckerOptionsWithLines file
let currentCode = string text
let! fantomasResponse =
formatDocumentAsync
{ SourceCode = currentCode
FilePath = filePath
Config = None }
match fantomasResponse with
| { Code = 1; Content = Some code } ->
fantomasLogger.debug (Log.setMessage (sprintf "Fantomas daemon was able to format \"%A\"" file))
return FormatDocumentResponse.Formatted(text, code)
| { Code = 2 } ->
fantomasLogger.debug (Log.setMessage (sprintf "\"%A\" did not change after formatting" file))
return FormatDocumentResponse.UnChanged
| { Code = 3; Content = Some error } ->
fantomasLogger.error (Log.setMessage (sprintf "Error while formatting \"%A\"\n%s" file error))
return FormatDocumentResponse.Error(sprintf "Formatting failed!\n%A" fantomasResponse)
| { Code = 4 } ->
fantomasLogger.debug (Log.setMessage (sprintf "\"%A\" was listed in a .fantomasignore file" file))
return FormatDocumentResponse.Ignored
| { Code = 6 } -> return FormatDocumentResponse.ToolNotPresent
| _ ->
fantomasLogger.warn (
Log.setMessage (
sprintf
"Fantomas daemon was unable to format \"%A\", due to unexpected result code %i\n%A"
file
fantomasResponse.Code
fantomasResponse
)
)
return FormatDocumentResponse.Error(sprintf "Formatting failed!\n%A" fantomasResponse)
with ex ->
fantomasLogger.warn (
Log.setMessage "Errors while formatting file, defaulting to previous content. Error message was {message}"
>> Log.addContextDestructured "message" ex.Message
>> Log.addExn ex
)
return! Core.Error ex.Message
}
let symbolImplementationProject
getProjectOptions
getUsesOfSymbol
getAllProjects
(tyRes: ParseAndCheckResults)
(pos: Position)
lineStr
=
let filterSymbols symbols =
symbols
|> Array.where (fun (su: FSharpSymbolUse) ->
su.IsFromDispatchSlotImplementation
|| (su.IsFromType
&& not (tyRes.GetParseResults.IsTypeAnnotationGivenAtPosition(su.Range.Start))))
async {
match tyRes.TryGetSymbolUseAndUsages pos lineStr with
| Ok (sym, usages) ->
let fsym = sym.Symbol
if fsym.IsPrivateToFile then
return CoreResponse.Res(LocationResponse.Use(sym, filterSymbols usages))
else if fsym.IsInternalToProject then
let opts = getProjectOptions tyRes.FileName
let! symbols = getUsesOfSymbol (tyRes.FileName, [ UMX.untag tyRes.FileName, opts ], sym.Symbol)
return CoreResponse.Res(LocationResponse.Use(sym, filterSymbols symbols))
else
let! symbols = getUsesOfSymbol (tyRes.FileName, getAllProjects (), sym.Symbol)
let symbols = filterSymbols symbols
return CoreResponse.Res(LocationResponse.Use(sym, filterSymbols symbols))
| Error e -> return CoreResponse.ErrorRes e
}
let typesig (tyRes: ParseAndCheckResults) (pos: Position) lineStr =
tyRes.TryGetToolTip pos lineStr
|> Result.bimap CoreResponse.Res CoreResponse.ErrorRes
// Calculates pipeline hints for now as in fSharp/pipelineHint with a bit of formatting on the hints
let inlineValues (contents: NamedText) (tyRes: ParseAndCheckResults) : Async<(pos * String)[]> =
asyncResult {
// Debug.waitForDebuggerAttached "AdaptiveServer"
let getSignatureAtPos pos =
option {
let! lineStr = contents.GetLine pos
let! tip = tyRes.TryGetToolTip pos lineStr |> Option.ofResult
return TipFormatter.extractGenericParameters tip
}
|> Option.defaultValue []
let areTokensCommentOrWhitespace (tokens: FSharpTokenInfo list) =
tokens
|> List.exists (fun token ->
token.CharClass <> FSharpTokenCharKind.Comment
&& token.CharClass <> FSharpTokenCharKind.WhiteSpace
&& token.CharClass <> FSharpTokenCharKind.LineComment)
|> not
let getStartingPipe =
function
| y :: xs when y.TokenName.ToUpper() = "INFIX_BAR_OP" -> Some y
| x :: y :: xs when x.TokenName.ToUpper() = "WHITESPACE" && y.TokenName.ToUpper() = "INFIX_BAR_OP" -> Some y
| _ -> None
let folder (lastExpressionLine, lastExpressionLineWasPipe, acc) (currentIndex, currentTokens) =
let isCommentOrWhitespace = areTokensCommentOrWhitespace currentTokens
let isPipe = getStartingPipe currentTokens
match isCommentOrWhitespace, isPipe with
| true, _ -> lastExpressionLine, lastExpressionLineWasPipe, acc
| false, Some pipe ->
currentIndex, true, (lastExpressionLine, lastExpressionLineWasPipe, currentIndex, pipe) :: acc
| false, None -> currentIndex, false, acc
// Signature looks like <T> is Async<unit>
let inline removeSignPrefix (s: String) =
s.Split(" is ") |> Array.tryLast |> Option.defaultValue ""
let hints =
Array.init ((contents: ISourceText).GetLineCount()) (fun line -> (contents: ISourceText).GetLineString line)
|> Array.map (Lexer.tokenizeLine [||])
|> Array.mapi (fun currentIndex currentTokens -> currentIndex, currentTokens)
|> Array.fold folder (0, false, [])
|> (fun (_, _, third) -> third |> Array.ofList)
|> Array.Parallel.map (fun (lastExpressionLine, lastExpressionLineWasPipe, currentIndex, pipeToken) ->
let pipePos = Position.fromZ currentIndex pipeToken.RightColumn
let prevLinePos = Position.fromZ lastExpressionLine 70 //We dont have the column on the previous line. So err to the right and let the client display in the right position
let gens = getSignatureAtPos pipePos
if lastExpressionLineWasPipe then
let allS = gens |> List.tryLast |> Option.defaultValue "" |> removeSignPrefix
[| (pipePos, allS) |]
else
match gens with
| [ currentS ] -> [| (pipePos, removeSignPrefix currentS) |]
| [ prevS; currentS ] ->
[| (prevLinePos, removeSignPrefix prevS)
(pipePos, removeSignPrefix currentS) |]
| _ ->
let allS = gens |> Seq.intersperse "; " |> Seq.reduce (+)
[| (pipePos, allS) |])
return (Array.concat hints)
}
|> AsyncResult.foldResult id (fun _ -> [||])
let pipelineHints (tryGetFileSource: _ -> Result<NamedText, _>) (tyRes: ParseAndCheckResults) =
result {
// Debug.waitForDebuggerAttached "AdaptiveServer"
let! contents = tryGetFileSource tyRes.FileName
let getSignatureAtPos pos =
option {
let! lineStr = contents.GetLine pos
let! tip = tyRes.TryGetToolTip pos lineStr |> Option.ofResult
return TipFormatter.extractGenericParameters tip
}
|> Option.defaultValue []
let areTokensCommentOrWhitespace (tokens: FSharpTokenInfo list) =
tokens
|> List.exists (fun token ->
token.CharClass <> FSharpTokenCharKind.Comment
&& token.CharClass <> FSharpTokenCharKind.WhiteSpace
&& token.CharClass <> FSharpTokenCharKind.LineComment)
|> not
let getStartingPipe =
function
| y :: xs when y.TokenName.ToUpper() = "INFIX_BAR_OP" -> Some y
| x :: y :: xs when x.TokenName.ToUpper() = "WHITESPACE" && y.TokenName.ToUpper() = "INFIX_BAR_OP" -> Some y
| _ -> None
let folder (lastExpressionLine, lastExpressionLineWasPipe, acc) (currentIndex, currentTokens) =
let isCommentOrWhitespace = areTokensCommentOrWhitespace currentTokens
let isPipe = getStartingPipe currentTokens
match isCommentOrWhitespace, isPipe with
| true, _ -> lastExpressionLine, lastExpressionLineWasPipe, acc
| false, Some pipe ->
currentIndex, true, (lastExpressionLine, lastExpressionLineWasPipe, currentIndex, pipe) :: acc
| false, None -> currentIndex, false, acc
let hints =
Array.init ((contents: ISourceText).GetLineCount()) (fun line -> (contents: ISourceText).GetLineString line)
|> Array.map (Lexer.tokenizeLine [||])
|> Array.mapi (fun currentIndex currentTokens -> currentIndex, currentTokens)
|> Array.fold folder (0, false, [])
|> (fun (_, _, third) -> third |> Array.ofList)
|> Array.Parallel.map (fun (lastExpressionLine, lastExpressionLineWasPipe, currentIndex, pipeToken) ->
let pipePos = Position.fromZ currentIndex pipeToken.RightColumn
let gens = getSignatureAtPos pipePos
let previousNonPipeLine =
if lastExpressionLineWasPipe then
None
else
Some lastExpressionLine
currentIndex, previousNonPipeLine, gens)
return CoreResponse.Res hints
}
|> Result.fold id (fun _ -> CoreResponse.InfoRes "Couldn't find file content")
let calculateNamespaceInsert
currentAst
(decl: DeclarationListItem)
(pos: Position)
getLine
: CompletionNamespaceInsert option =
let getLine (p: Position) = getLine p |> Option.defaultValue ""
let idents = decl.FullName.Split '.'
decl.NamespaceToOpen
|> Option.bind (fun n ->
(currentAst ())
|> Option.map (fun ast ->
ParsedInput.FindNearestPointToInsertOpenDeclaration (pos.Line) ast idents OpenStatementInsertionPoint.Nearest)
|> Option.map (fun ic ->
//TODO: unite with `CodeFix/ResolveNamespace`
//TODO: Handle Nearest AND TopLevel. Currently it's just Nearest (vs. ResolveNamespace -> TopLevel) (#789)
let detectIndentation (line: string) =
line |> Seq.takeWhile ((=) ' ') |> Seq.length
// adjust line
let pos =
match ic.ScopeKind with
| ScopeKind.Namespace ->
// for namespace `open` isn't created close at namespace,
// but instead on first member
// -> move `open` closer to namespace
// this only happens when there are no other `open`
// from insert position go up until first open OR namespace
ic.Pos.LinesToBeginning()
|> Seq.tryFind (fun l ->
let lineStr = getLine l
// namespace MUST be top level -> no indentation
lineStr.StartsWith "namespace ")
|> function
// move to the next line below "namespace"
| Some l -> l.IncLine()
| None -> ic.Pos
| _ -> ic.Pos
// adjust column
let pos =
match pos with
| Pos (1, c) -> pos
| Pos (l, 0) ->
let prev = getLine (pos.DecLine())
let indentation = detectIndentation prev
if indentation <> 0 then
// happens when there are already other `open`s
Position.mkPos l indentation
else
pos
| Pos (_, c) -> pos
{ Namespace = n
Position = pos
Scope = ic.ScopeKind }))
/// * `includeDeclarations`:
/// if `false` only returns usage locations and excludes declarations
/// * Note: if `true` you can still separate usages and declarations from each other
/// with `Symbol.partitionInfoDeclarationsAndUsages`
/// * `includeBackticks`:
/// if `true` returns ranges including existing backticks, otherwise without:
/// `let _ = ``my value`` + 42`
/// * `true`: ` ``my value`` `
/// * `false`: `my value`
/// * `errorOnFailureToFixRange`:
/// Ranges returned by FCS don't just span the actual identifier, but include Namespace, Module, Type: `System.String.IsNullOrEmpty`
/// These ranges gets adjusted to include just the concrete identifier (`IsNullOrEmpty`)
/// * If `false` and range cannot be adjust, the original range gets used.
/// * When results are more important than always exact range
/// -> for "Find All References"
/// * If `true`: Instead of using the source range, this function instead returns an Error
/// * When exact ranges are required
/// -> for "Rename"
let symbolUseWorkspace
(getDeclarationLocation: FSharpSymbolUse * NamedText -> SymbolDeclarationLocation option)
(findReferencesForSymbolInFile: (string<LocalPath> * FSharpProjectOptions * FSharpSymbol) -> Async<Range seq>)
(tryGetFileSource: string<LocalPath> -> ResultOrString<NamedText>)
(tryGetProjectOptionsForFsproj: string<LocalPath> -> FSharpProjectOptions option)
(getAllProjectOptions: unit -> FSharpProjectOptions seq)
(includeDeclarations: bool)
(includeBackticks: bool)
(errorOnFailureToFixRange: bool)
pos
lineStr
(text: NamedText)
(tyRes: ParseAndCheckResults)
: Async<Result<(FSharpSymbol * IDictionary<string<LocalPath>, Range[]>), string>> =
asyncResult {
let! symbolUse = tyRes.TryGetSymbolUse pos lineStr |> Result.ofOption (fun _ -> "No symbol")
let symbol = symbolUse.Symbol
let symbolNameCore = symbol.DisplayNameCore
let tryAdjustRanges (text: NamedText, ranges: seq<Range>) =
let ranges = ranges |> Seq.map (fun range -> range.NormalizeDriveLetterCasing())
if errorOnFailureToFixRange then
ranges
|> Seq.map (fun range ->
Tokenizer.tryFixupRange (symbolNameCore, range, text, includeBackticks)
|> Result.ofVOption (fun _ -> $"Cannot adjust range"))
|> Seq.sequenceResultM
|> Result.map (Seq.toArray)
else
ranges
|> Seq.map (fun range ->
Tokenizer.tryFixupRange (symbolNameCore, range, text, includeBackticks)
|> ValueOption.defaultValue range)
|> Seq.toArray
|> Ok
let declLoc = getDeclarationLocation (symbolUse, text)
match declLoc with
// local symbol -> all uses are inside `text`
// Note: declarations in script files are currently always local!
| Some SymbolDeclarationLocation.CurrentDocument ->
let! ct = Async.CancellationToken
let symbolUses = tyRes.GetCheckResults.GetUsesOfSymbolInFile(symbol, ct)
let symbolUses: _ seq =
if includeDeclarations then
symbolUses
else
symbolUses |> Seq.filter (fun u -> not u.IsFromDefinition)
let ranges = symbolUses |> Seq.map (fun u -> u.Range)
// Note: tryAdjustRanges is designed to only be able to fail iff `errorOnFailureToFixRange` is `true`
let! ranges = tryAdjustRanges (text, ranges)
let ranges = dict [ (text.FileName, Seq.toArray ranges) ]
return (symbol, ranges)
| scope ->
let projectsToCheck =
match scope with
| Some (SymbolDeclarationLocation.Projects (projects (*isLocalForProject=*) , true)) -> projects
| Some (SymbolDeclarationLocation.Projects (projects (*isLocalForProject=*) , false)) ->
[ for project in projects do
yield project
yield!
project.ReferencedProjects
|> Array.choose (fun p -> UMX.tag p.OutputFile |> tryGetProjectOptionsForFsproj) ]
|> List.distinctBy (fun x -> x.ProjectFileName)
| _ (*None*) ->
// symbol is declared external -> look through all F# projects
// (each script (including untitled) has its own project -> scripts get checked too. But only once they are loaded (-> inside `state`))
getAllProjectOptions ()
|> Seq.distinctBy (fun x -> x.ProjectFileName)
|> Seq.toList
let tryAdjustRanges (file: string<LocalPath>, ranges: Range[]) =
match tryGetFileSource file with
| Error _ when errorOnFailureToFixRange -> Error $"Cannot get source of '{file}'"
| Error _ -> Ok ranges
| Ok text ->
tryAdjustRanges (text, ranges)
// Note: `Error` only possible when `errorOnFailureToFixRange`
|> Result.mapError (fun _ -> $"Cannot adjust ranges in file '{file}'")
let isDeclLocation =
if includeDeclarations then
// not actually used
fun _ -> false
else
symbol |> Symbol.getIsDeclaration
let dict = Dictionary()
/// Adds References of `symbol` in `file` to `dict`
///
/// `Error` iff adjusting ranges failed (including cannot get source) and `errorOnFailureToFixRange`. Otherwise always `Ok`
let tryFindReferencesInFile (file: string<LocalPath>, project: FSharpProjectOptions) =
async {
if dict.ContainsKey file then
return Ok()
else
let! references = findReferencesForSymbolInFile (file, project, symbol)
let references =
if includeDeclarations then
references
else
references |> Seq.filter (not << isDeclLocation)
let references = references |> Seq.toArray
// Note: this check is important!
// otherwise `tryAdjustRanges` tries to get source for files like `AssemblyInfo.fs`
// (which fails -> error if `errorOnFailureToFixRange`)
if references |> Array.isEmpty then
return Ok()
else
let ranges = tryAdjustRanges (file, references)
match ranges with
| Error msg when errorOnFailureToFixRange -> return Error msg
| Error _ ->
dict.TryAdd(file, references) |> ignore
return Ok()
| Ok ranges ->
dict.TryAdd(file, ranges) |> ignore
return Ok()
}
|> Async.Catch
|> Async.map (Result.ofChoice >> Result.mapError string >> Result.bind id)
|> Async.map (fun x ->
match x with
| Ok () -> ()
| Error e ->
commandsLogger.info (
Log.setMessage "tryFindReferencesInFile failed: {error}"
>> Log.addContextDestructured "error" e
))
let iterProject (project: FSharpProjectOptions) =
asyncResult {
//Enhancement: do in parallel?
for file in project.SourceFiles do
let file = UMX.tag file
do! tryFindReferencesInFile (file, project)
}
let iterProjects (projects: FSharpProjectOptions seq) =
asyncResult {
for project in projects do
do! iterProject project
}
do! iterProjects projectsToCheck
return (symbol, dict)
}
/// Puts `newName` into backticks if necessary.
///
///
/// Also does very basic validation of `newName`:
/// * Must be valid operator name when operator
let adjustRenameSymbolNewName pos lineStr (text: NamedText) (tyRes: ParseAndCheckResults) (newName: string) =
asyncResult {
let! symbolUse =
tyRes.TryGetSymbolUse pos lineStr
|> Result.ofOption (fun _ -> "Nothing to rename")
match symbolUse with
| SymbolUse.Operator _ ->
// different validation rules
// and no backticks for operator
if PrettyNaming.IsOperatorDisplayName newName then
return newName
else
return! Error $"'%s{newName}' is not a valid operator name!"
| _ ->
//ENHANCEMENT: more validation like check upper case start for types
// `IsIdentifierName` doesn't work with backticks
// -> only check if no backticks
let newBacktickedName = newName |> PrettyNaming.AddBackticksToIdentifierIfNeeded
if newBacktickedName.StartsWith "``" && newBacktickedName.EndsWith "``" then
return newBacktickedName
elif PrettyNaming.IsIdentifierName newName then
return newName
else
return! Error $"'%s{newName}' is not a valid identifier name!"
}
/// `Error` if renaming is invalid at specified `pos`.
/// Otherwise range of identifier at specified `pos`
///
/// Note:
/// Rename for Active Patterns is disabled:
/// Each case is its own identifier and complete Active Pattern name isn't correctly handled by FCS
///
/// Note:
/// Rename for Active Pattern Cases is disabled:
/// `SymbolUseWorkspace` returns ranges for ALL Cases of that Active Pattern instead of just the single case
let renameSymbolRange
(getDeclarationLocation: FSharpSymbolUse * NamedText -> SymbolDeclarationLocation option)
(includeBackticks: bool)
pos
lineStr
(text: NamedText)
(tyRes: ParseAndCheckResults)
=
asyncResult {
let! symbolUse =
tyRes.TryGetSymbolUse pos lineStr
|> Result.ofOption (fun _ -> "Nothing to rename")
let! _ =
// None: external symbol -> not under our control -> cannot rename
getDeclarationLocation (symbolUse, text)
|> Result.ofOption (fun _ -> "Must be declared inside current workspace, but is external.")
do!
match symbolUse with
| SymbolUse.ActivePattern _ ->
// Active Pattern is not supported:
// ```fsharp
// let (|Even|Odd|) v = if v % 2 = 0 then Even else Odd
// match 42 with
// | Even -> ()
// | Odd -> ()
// let _ = (|Even|Odd|) 42
// ```
// ->
// `(|Even|Odd|)` at usage is own symbol -- NOT either Even or Odd (depending on pos)
// -> Rename at that location renames complete `(|Even|Odd|)` -- but not individual usages
Error "Renaming of Active Patterns is not supported"
| SymbolUse.ActivePatternCase _ ->
// Active Pattern Case is not supported:
// ```fsharp
// let (|Even|Odd|) v = if v % 2 = 0 then Even else Odd
// match 42 with
// | Even -> ()
// | Odd -> ()
// ```
// -> `Even` -> finds all occurrences of `Odd` too -> get renamed too...
//Enhancement: Handle: Exclude cases that don't match symbol at pos
Error "Renaming of Active Pattern Cases is currently not supported"
| _ -> Ok()
let symbol = symbolUse.Symbol
let nameCore = symbol.DisplayNameCore
let! range =
Tokenizer.tryFixupRange (nameCore, symbolUse.Range, text, includeBackticks)
|> Result.ofVOption (fun _ -> "Cannot correctly isolate range of identifier")
return (symbol, nameCore, range)
}
// given an enveloping range and the sub-ranges it overlaps, split out the enveloping range into a
// set of range segments that are non-overlapping with the children
let segmentRanges (parentRange: Range) (childRanges: Range[]) : Range[] =
let firstSegment =
Range.mkRange parentRange.FileName parentRange.Start childRanges.[0].Start
let lastSegment =
Range.mkRange parentRange.FileName (Array.last childRanges).End parentRange.End // from end of last child to end of parent
// now we can go pairwise, emitting a new range for the area between each end and start
let innerSegments =
childRanges
|> Array.pairwise
|> Array.map (fun (left, right) -> Range.mkRange parentRange.FileName left.End right.Start)