Skip to content

Commit 470b049

Browse files
committed
Add support for active patterns in reference finding and enhance tests
- Implemented logic to search for both active pattern functions and their cases in reference finding. - Added tests for active pattern usage, including partial and total patterns. - Created new project structure for active pattern test cases. - Updated existing tests to include active pattern references across modules.
1 parent e65df5d commit 470b049

File tree

10 files changed

+573
-2
lines changed

10 files changed

+573
-2
lines changed

src/FsAutoComplete.Core/Commands.fs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,57 @@ module Commands =
841841

842842
let dict = ConcurrentDictionary()
843843

844+
// For active patterns, we need to search for both the function and its cases
845+
// because FCS doesn't always find cross-file references correctly
846+
let symbolsToSearch =
847+
match symbol with
848+
| :? FSharpActivePatternCase as apCase ->
849+
// At a usage site: we have a case, also search for the declaring function
850+
// e.g., for case `ParseInt` in `(|ParseInt|_|)`, also search for the function
851+
match apCase.Group.DeclaringEntity with
852+
| Some entity ->
853+
// Search for the function using the pattern "|CaseName|" to match within the full pattern name
854+
// e.g., for case "ParseInt", search for function containing "|ParseInt|"
855+
let apcSearchString = $"|{apCase.DisplayName}|"
856+
857+
let declaringMember =
858+
try
859+
entity.MembersFunctionsAndValues
860+
|> Seq.tryFind (fun m ->
861+
m.DisplayName.Contains(apcSearchString, System.StringComparison.OrdinalIgnoreCase)
862+
|| m.CompiledName.Contains(apCase.DisplayName, System.StringComparison.OrdinalIgnoreCase))
863+
with _ ->
864+
None
865+
866+
match declaringMember with
867+
| Some m -> [ symbol; m :> FSharpSymbol ]
868+
| None -> [ symbol ]
869+
| None -> [ symbol ]
870+
| :? FSharpMemberOrFunctionOrValue as mfv when mfv.IsActivePattern ->
871+
// At a declaration site: this is an active pattern function
872+
// We need to search for both the function AND its individual cases to find all usages
873+
// FCS finds function-call style usages (e.g., `(|ParseFloat|_|) x`) for the function
874+
// FCS finds match-case style usages (e.g., `| ParseFloat x ->`) for the cases
875+
match mfv.DeclaringEntity with
876+
| Some entity ->
877+
// Get all active pattern cases in the entity and find those matching this function's cases
878+
let functionCases =
879+
try
880+
entity.ActivePatternCases
881+
|> Seq.filter (fun apc ->
882+
// Match case to function by checking if the function's display name contains the case name
883+
// e.g., for function "|ParseFloat|_|", case would be "ParseFloat"
884+
mfv.DisplayName.Contains($"|{apc.DisplayName}|", System.StringComparison.OrdinalIgnoreCase))
885+
|> Seq.map (fun apc -> apc :> FSharpSymbol)
886+
|> Seq.toList
887+
with _ ->
888+
[]
889+
symbol :: functionCases
890+
| None -> [ symbol ]
891+
| :? FSharpMemberOrFunctionOrValue ->
892+
[ symbol ]
893+
| _ -> [ symbol ]
894+
844895
/// Adds References of `symbol` in `file` to `dict`
845896
///
846897
/// `Error` iff adjusting ranges failed (including cannot get source) and `errorOnFailureToFixRange`. Otherwise always `Ok`
@@ -849,7 +900,20 @@ module Commands =
849900
if dict.ContainsKey file then
850901
return Ok()
851902
else
852-
let! references = findReferencesForSymbolInFile (file, project, symbol)
903+
// Search for all related symbols (for active pattern cases, includes the declaring member)
904+
let! allReferences =
905+
symbolsToSearch
906+
|> List.map (fun s -> findReferencesForSymbolInFile (file, project, s) |> Async.map Seq.toArray)
907+
|> Async.Parallel
908+
909+
let references =
910+
allReferences
911+
|> Array.concat
912+
// Deduplicate - when searching for multiple symbols (e.g., active pattern function + cases),
913+
// they may return duplicate ranges at the same location
914+
// Use distinctBy with a tuple key since Range objects may not be structurally equal
915+
|> Array.distinctBy (fun r -> r.StartLine, r.StartColumn, r.EndLine, r.EndColumn)
916+
|> Array.toSeq
853917

854918
let references =
855919
if includeDeclarations then

test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,55 @@ let private rangeTests state =
609609
| MyModule.$<Even>$ -> ()
610610
| MyModule.Odd -> ()
611611
"""
612+
testCaseAsync "can get range of partial Active Pattern"
613+
<|
614+
// Partial active pattern: `(|ParseInt|_|)` - returns Option
615+
// The `|_|` indicates it's partial (can fail to match)
616+
checkRanges
617+
server
618+
"""
619+
module MyModule =
620+
let ($D<|Pa$0rseInt|_|>D$) (input: string) =
621+
match System.Int32.TryParse input with
622+
| true, v -> Some v
623+
| false, _ -> None
624+
625+
open MyModule
626+
let _ = ($<|ParseInt|_|>$) "42"
627+
let _ = MyModule.($<|ParseInt|_|>$) "42"
628+
let _ =
629+
match "42" with
630+
| ParseInt v -> v
631+
| _ -> 0
632+
let _ =
633+
match "42" with
634+
| MyModule.ParseInt v -> v
635+
| _ -> 0
636+
"""
637+
testCaseAsync "can get range of partial Active Pattern case"
638+
<|
639+
// When clicking on the case name in a partial active pattern
640+
checkRanges
641+
server
642+
"""
643+
module MyModule =
644+
let (|$D<ParseInt>D$|_|) (input: string) =
645+
match System.Int32.TryParse input with
646+
| true, v -> Some v
647+
| false, _ -> None
648+
649+
open MyModule
650+
let _ = (|ParseInt|_|) "42"
651+
let _ = MyModule.(|ParseInt|_|) "42"
652+
let _ =
653+
match "42" with
654+
| $<Par$0seInt>$ v -> v
655+
| _ -> 0
656+
let _ =
657+
match "42" with
658+
| MyModule.$<ParseInt>$ v -> v
659+
| _ -> 0
660+
"""
612661
testCaseAsync "can get range of type for static function call"
613662
<| checkRanges
614663
server
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
</PropertyGroup>
6+
7+
<ItemGroup>
8+
<Compile Include="Patterns.fs" />
9+
<Compile Include="Module1.fs" />
10+
<Compile Include="Module2.fs" />
11+
<Compile Include="Program.fs" />
12+
</ItemGroup>
13+
14+
</Project>
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
namespace ActivePatternProject
2+
3+
/// First module that uses patterns from Patterns module
4+
module Module1 =
5+
open Patterns
6+
7+
// Using total active pattern Even|Odd
8+
let classifyNumber n =
9+
match n with
10+
| Even -> "even"
11+
| Odd -> "odd"
12+
13+
// Using total active pattern as function
14+
let getEvenOdd n = (|Even|Odd|) n
15+
16+
// Using partial active pattern ParseInt
17+
let tryParseNumber input =
18+
match input with
19+
| ParseInt n -> Some n
20+
| _ -> None
21+
22+
// Using partial active pattern as function
23+
let parseIntDirect input = (|ParseInt|_|) input
24+
25+
// Using ParseFloat partial active pattern
26+
let tryParseFloat input =
27+
match input with
28+
| ParseFloat f -> Some f
29+
| _ -> None
30+
31+
// Using ParseFloat as function
32+
let parseFloatDirect input = (|ParseFloat|_|) input
33+
34+
// Using parameterized active pattern
35+
let isDivisibleBy3 n =
36+
match n with
37+
| DivisibleBy 3 result -> Some result
38+
| _ -> None
39+
40+
// Using multiple patterns in one match
41+
let analyzeNumber n =
42+
match n with
43+
| Even & Positive -> "even positive"
44+
| Even & Negative -> "even negative"
45+
| Odd & Positive -> "odd positive"
46+
| Odd & Negative -> "odd negative"
47+
| Zero -> "zero"
48+
49+
// Using Positive|Negative|Zero pattern
50+
let getSign n =
51+
match n with
52+
| Positive -> 1
53+
| Negative -> -1
54+
| Zero -> 0
55+
56+
// ============================================
57+
// STRUCT PARTIAL ACTIVE PATTERNS
58+
// ============================================
59+
60+
// Using struct partial active pattern ParseIntStruct
61+
let tryParseNumberStruct input =
62+
match input with
63+
| ParseIntStruct n -> ValueSome n
64+
| _ -> ValueNone
65+
66+
// Using struct partial active pattern as function
67+
let parseIntStructDirect input = (|ParseIntStruct|_|) input
68+
69+
// Using ParseFloatStruct partial active pattern
70+
let tryParseFloatStruct input =
71+
match input with
72+
| ParseFloatStruct f -> ValueSome f
73+
| _ -> ValueNone
74+
75+
// Using ParseFloatStruct as function
76+
let parseFloatStructDirect input = (|ParseFloatStruct|_|) input
77+
78+
// Using NonEmptyStruct partial active pattern
79+
let validateInputStruct input =
80+
match input with
81+
| NonEmptyStruct s -> ValueSome s
82+
| _ -> ValueNone
83+
84+
// Using struct parameterized active pattern
85+
let isDivisibleBy3Struct n =
86+
match n with
87+
| DivisibleByStruct 3 result -> ValueSome result
88+
| _ -> ValueNone
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
namespace ActivePatternProject
2+
3+
/// Second module - uses patterns with qualified access
4+
module Module2 =
5+
6+
// Using patterns with fully qualified names
7+
let classifyWithQualified n =
8+
match n with
9+
| Patterns.Even -> "even"
10+
| Patterns.Odd -> "odd"
11+
12+
// Using partial pattern with qualified name
13+
let parseWithQualified input =
14+
match input with
15+
| Patterns.ParseInt n -> Some n
16+
| _ -> None
17+
18+
// Using the pattern as a function with qualified name
19+
let parseIntQualified input = Patterns.(|ParseInt|_|) input
20+
let evenOddQualified n = Patterns.(|Even|Odd|) n
21+
22+
// Using Regex pattern
23+
let matchEmail input =
24+
match input with
25+
| Patterns.Regex @"[\w.]+@[\w.]+" email -> Some email
26+
| _ -> None
27+
28+
// Using NonEmpty pattern
29+
let validateInput input =
30+
match input with
31+
| Patterns.NonEmpty s -> Ok s
32+
| _ -> Error "Input cannot be empty"
33+
34+
// Complex example with multiple patterns
35+
let processInput input =
36+
match input with
37+
| Patterns.NonEmpty s ->
38+
match s with
39+
| Patterns.ParseInt n ->
40+
match n with
41+
| Patterns.Even -> "parsed even number"
42+
| Patterns.Odd -> "parsed odd number"
43+
| Patterns.ParseFloat f -> sprintf "parsed float: %f" f
44+
| _ -> "non-numeric string"
45+
| _ -> "empty input"
46+
47+
// Using DivisibleBy with different parameters
48+
let checkDivisibility n =
49+
match n with
50+
| Patterns.DivisibleBy 2 _ -> "divisible by 2"
51+
| Patterns.DivisibleBy 3 _ -> "divisible by 3"
52+
| Patterns.DivisibleBy 5 _ -> "divisible by 5"
53+
| _ -> "not divisible by 2, 3, or 5"
54+
55+
// ============================================
56+
// STRUCT PARTIAL ACTIVE PATTERNS (qualified access)
57+
// ============================================
58+
59+
// Using struct partial pattern with qualified name
60+
let parseWithQualifiedStruct input =
61+
match input with
62+
| Patterns.ParseIntStruct n -> ValueSome n
63+
| _ -> ValueNone
64+
65+
// Using struct pattern as a function with qualified name
66+
let parseIntStructQualified input = Patterns.(|ParseIntStruct|_|) input
67+
let parseFloatStructQualified input = Patterns.(|ParseFloatStruct|_|) input
68+
69+
// Complex example with struct patterns
70+
let processInputStruct input =
71+
match input with
72+
| Patterns.NonEmptyStruct s ->
73+
match s with
74+
| Patterns.ParseIntStruct n ->
75+
match n with
76+
| Patterns.Even -> "parsed even number (struct)"
77+
| Patterns.Odd -> "parsed odd number (struct)"
78+
| Patterns.ParseFloatStruct f -> sprintf "parsed float (struct): %f" f
79+
| _ -> "non-numeric string"
80+
| _ -> "empty input"
81+
82+
// Using struct DivisibleBy with different parameters
83+
let checkDivisibilityStruct n =
84+
match n with
85+
| Patterns.DivisibleByStruct 2 _ -> "divisible by 2 (struct)"
86+
| Patterns.DivisibleByStruct 3 _ -> "divisible by 3 (struct)"
87+
| Patterns.DivisibleByStruct 5 _ -> "divisible by 5 (struct)"
88+
| _ -> "not divisible by 2, 3, or 5"

0 commit comments

Comments
 (0)