diff --git a/src/FsAutoComplete.Core/Commands.fs b/src/FsAutoComplete.Core/Commands.fs index c4af5c608..f77e8d27f 100644 --- a/src/FsAutoComplete.Core/Commands.fs +++ b/src/FsAutoComplete.Core/Commands.fs @@ -841,6 +841,64 @@ module Commands = let dict = ConcurrentDictionary() + // For active patterns, we need to search for both the function and its cases + // because FCS doesn't always find cross-file references correctly. + // The function symbol finds function-call style usages, the case symbol finds match-case usages. + let symbolsToSearch = + match symbol with + | :? FSharpActivePatternCase as apCase -> + // At a case symbol (either at declaration or usage) + // Search for both the case (for match-case usages) AND the function (for function-call usages) + match apCase.Group.DeclaringEntity with + | Some entity -> + let apcSearchString = $"|{apCase.DisplayName}|" + + let declaringMember = + try + entity.MembersFunctionsAndValues + |> Seq.tryFind (fun m -> + m.DisplayName.Contains(apcSearchString, System.StringComparison.OrdinalIgnoreCase) + || m.CompiledName.Contains(apCase.DisplayName, System.StringComparison.OrdinalIgnoreCase)) + with e -> + commandsLogger.debug ( + Log.setMessage "Failed to find declaring member for active pattern case {case}: {error}" + >> Log.addContextDestructured "case" apCase.DisplayName + >> Log.addExn e + ) + + None + + match declaringMember with + | Some m -> [ symbol; m :> FSharpSymbol ] + | None -> [ symbol ] + | None -> [ symbol ] + | :? FSharpMemberOrFunctionOrValue as mfv when mfv.IsActivePattern -> + // At an active pattern function symbol - search for both the function AND its cases + // FCS finds function-call style usages (e.g., `(|ParseFloat|_|) x`) for the function + // FCS finds match-case style usages (e.g., `| ParseFloat x ->`) for the cases + match mfv.DeclaringEntity with + | Some entity -> + let functionCases = + try + entity.ActivePatternCases + |> Seq.filter (fun apc -> + mfv.DisplayName.Contains($"|{apc.DisplayName}|", System.StringComparison.OrdinalIgnoreCase)) + |> Seq.map (fun apc -> apc :> FSharpSymbol) + |> Seq.toList + with e -> + commandsLogger.debug ( + Log.setMessage "Failed to find cases for active pattern function {func}: {error}" + >> Log.addContextDestructured "func" mfv.DisplayName + >> Log.addExn e + ) + + [] + + symbol :: functionCases + | None -> [ symbol ] + | :? FSharpMemberOrFunctionOrValue -> [ symbol ] + | _ -> [ symbol ] + /// Adds References of `symbol` in `file` to `dict` /// /// `Error` iff adjusting ranges failed (including cannot get source) and `errorOnFailureToFixRange`. Otherwise always `Ok` @@ -849,7 +907,37 @@ module Commands = if dict.ContainsKey file then return Ok() else - let! references = findReferencesForSymbolInFile (file, project, symbol) + // Search for all related symbols (for active pattern cases, includes the declaring member) + let! allReferences = + symbolsToSearch + |> List.map (fun s -> findReferencesForSymbolInFile (file, project, s) |> Async.map Seq.toArray) + |> Async.Parallel + + let references = + allReferences + |> Array.concat + // Deduplicate overlapping ranges - when searching for multiple symbols (e.g., active pattern + // function + cases), they may return overlapping ranges at the same location. + // For example, at an active pattern declaration we might find both: + // `|IsOneOfChoice|_|` (function symbol) and `IsOneOfChoice` (case symbol) + // Keep only the outermost (longest) range when ranges overlap or are contained within each other. + // + // Note: This deduplication assumes active pattern references are single-line identifiers, + // so we only need to compare column positions within each line. Multiline ranges would + // require comparing StartLine/EndLine as well, but active pattern names cannot span lines. + |> Array.groupBy (fun r -> r.StartLine) + |> Array.collect (fun (_, rangesOnLine) -> + // For ranges on the same line, filter out those that are contained within another + rangesOnLine + |> Array.filter (fun r -> + rangesOnLine + |> Array.exists (fun other -> + // Check if 'other' strictly contains 'r' (r is nested inside other) + other.StartColumn <= r.StartColumn + && other.EndColumn >= r.EndColumn + && (other.StartColumn < r.StartColumn || other.EndColumn > r.EndColumn)) + |> not)) + |> Array.toSeq let references = if includeDeclarations then @@ -971,6 +1059,16 @@ module Commands = else result.Add(k, v) + // Final deduplication pass: remove exact duplicate ranges across all symbol searches. + // This is separate from the per-file deduplication in tryFindReferencesInFile which removes + // *contained* ranges (e.g., `IsOneOfChoice` inside `|IsOneOfChoice|_|`). + // This pass removes *identical* ranges that may arise when TryGetSymbolUses returns both + // the function and case symbol pointing to the same location. + for KeyValue(k, v) in result do + result.[k] <- + v + |> Array.distinctBy (fun r -> r.StartLine, r.StartColumn, r.EndLine, r.EndColumn) + return result } diff --git a/test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs b/test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs index 7b352af74..eca7edfca 100644 --- a/test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/FindReferencesTests.fs @@ -371,6 +371,109 @@ let private solutionTests state = Expect.locationsEqual getSource false refs expected }) ]) ]) +/// Tests for ActivePatternProject - tests cross-file active pattern references +let private activePatternProjectTests state = + + let marker = "//>" + + let readReferences path = + let lines = File.ReadAllLines path + let refs = Dictionary>() + + for i in 0 .. (lines.Length - 1) do + let line = lines[i].TrimStart() + + if line.StartsWith(marker, StringComparison.Ordinal) then + let l = line.Substring(marker.Length).Trim() + let splits = l.Split([| ' ' |], 2) + let mark = splits[0] + + let range = + let col = line.IndexOf(mark, StringComparison.Ordinal) + let length = mark.Length + let line = i - 1 // marker is line AFTER actual range + + { Start = + { Line = uint32 line + Character = uint32 col } + End = + { Line = uint32 line + Character = uint32 (col + length) } } + + let loc = + { Uri = path |> normalizePath |> Path.LocalPathToUri + Range = range } + + let name = if splits.Length > 1 then splits[1] else "" + + if not (refs.ContainsKey name) then + refs[name] <- List<_>() + + let existing = refs[name] + existing.Add loc |> ignore + + refs + + let readAllReferences dir = + let files = Directory.GetFiles(dir, "*.fs", SearchOption.AllDirectories) + + files + |> Seq.map readReferences + |> Seq.map (fun dict -> dict |> Seq.map (fun kvp -> kvp.Key, kvp.Value)) + |> Seq.collect id + |> Seq.groupBy fst + |> Seq.map (fun (name, locs) -> (name, locs |> Seq.map snd |> Seq.collect id |> Seq.toArray)) + |> Seq.map (fun (name, locs) -> {| Name = name; Locations = locs |}) + |> Seq.toArray + + + let path = + Path.Combine(__SOURCE_DIRECTORY__, "TestCases", "FindReferences", "ActivePatternProject") + + serverTestList "ActivePatternProject" state defaultConfigDto (Some path) (fun server -> + [ + let mainDoc = "Patterns.fs" + + documentTestList "inside Patterns.fs" server (Server.openDocument mainDoc) (fun doc -> + [ let refs = readAllReferences path + + for r in refs do + testCaseAsync + r.Name + (async { + let! (doc, _) = doc + + let cursor = + let cursor = + r.Locations + |> Seq.filter (fun l -> l.Uri = doc.Uri) + |> Seq.minBy (fun l -> l.Range.Start) + + cursor.Range.Start + + let request: ReferenceParams = + { TextDocument = doc.TextDocumentIdentifier + Position = cursor + Context = { IncludeDeclaration = true } + WorkDoneToken = None + PartialResultToken = None } + + let! refs = doc.Server.Server.TextDocumentReferences request + + let refs = + refs + |> Flip.Expect.wantOk "Should not fail" + |> Flip.Expect.wantSome "Should return references" + + let expected = r.Locations + + let getSource uri = + let path = Path.FileUriToLocalPath uri + File.ReadAllText path + + Expect.locationsEqual getSource false refs expected + }) ]) ]) + /// multiple untitled files (-> all docs are unrelated) /// -> Tests for external symbols (-> over all docs) & symbol just in current doc (-> no matches in other unrelated docs) let private untitledTests state = @@ -609,6 +712,356 @@ let private rangeTests state = | MyModule.$$ -> () | MyModule.Odd -> () """ + testCaseAsync "can get range of partial Active Pattern" + <| + // Partial active pattern: `(|ParseInt|_|)` - returns Option + // The `|_|` indicates it's partial (can fail to match) + checkRanges + server + """ + module MyModule = + let ($D<|Pa$0rseInt|_|>D$) (input: string) = + match System.Int32.TryParse input with + | true, v -> Some v + | false, _ -> None + + open MyModule + let _ = ($<|ParseInt|_|>$) "42" + let _ = MyModule.($<|ParseInt|_|>$) "42" + let _ = + match "42" with + | ParseInt v -> v + | _ -> 0 + let _ = + match "42" with + | MyModule.ParseInt v -> v + | _ -> 0 + """ + testCaseAsync "can get range of partial Active Pattern case" + <| + // When clicking on the case name in a partial active pattern + checkRanges + server + """ + module MyModule = + let (|$DD$|_|) (input: string) = + match System.Int32.TryParse input with + | true, v -> Some v + | false, _ -> None + + open MyModule + let _ = (|ParseInt|_|) "42" + let _ = MyModule.(|ParseInt|_|) "42" + let _ = + match "42" with + | $$ v -> v + | _ -> 0 + let _ = + match "42" with + | MyModule.$$ v -> v + | _ -> 0 + """ + testCaseAsync "can get range of struct partial Active Pattern" + <| + // Struct partial active pattern: `(|ParseIntStruct|_|)` - returns ValueOption + // These use ValueSome/ValueNone for better performance (no heap allocation) + checkRanges + server + """ + module MyModule = + let ($D<|Pa$0rseIntStruct|_|>D$) (input: string) = + match System.Int32.TryParse input with + | true, v -> ValueSome v + | false, _ -> ValueNone + + open MyModule + let _ = ($<|ParseIntStruct|_|>$) "42" + let _ = MyModule.($<|ParseIntStruct|_|>$) "42" + let _ = + match "42" with + | ParseIntStruct v -> v + | _ -> 0 + let _ = + match "42" with + | MyModule.ParseIntStruct v -> v + | _ -> 0 + """ + testCaseAsync "can get range of struct partial Active Pattern case" + <| + // When clicking on the case name in a struct partial active pattern + checkRanges + server + """ + module MyModule = + let (|$DD$|_|) (input: string) = + match System.Int32.TryParse input with + | true, v -> ValueSome v + | false, _ -> ValueNone + + open MyModule + let _ = (|ParseIntStruct|_|) "42" + let _ = MyModule.(|ParseIntStruct|_|) "42" + let _ = + match "42" with + | $$ v -> v + | _ -> 0 + let _ = + match "42" with + | MyModule.$$ v -> v + | _ -> 0 + """ + testCaseAsync "can get range of parameterized Active Pattern" + <| + // Parameterized active pattern: `(|DivisibleBy|_|) divisor value` + // Takes an extra parameter before the input + checkRanges + server + """ + module MyModule = + let ($D<|Divi$0sibleBy|_|>D$) divisor value = + if value % divisor = 0 then Some(value / divisor) + else None + + open MyModule + let _ = ($<|DivisibleBy|_|>$) 3 9 + let _ = MyModule.($<|DivisibleBy|_|>$) 3 9 + let _ = + match 9 with + | DivisibleBy 3 q -> q + | _ -> 0 + let _ = + match 9 with + | MyModule.DivisibleBy 3 q -> q + | _ -> 0 + """ + testCaseAsync "can get range of parameterized Active Pattern case" + <| + // When clicking on the case name in a parameterized active pattern + checkRanges + server + """ + module MyModule = + let (|$DD$|_|) divisor value = + if value % divisor = 0 then Some(value / divisor) + else None + + open MyModule + let _ = (|DivisibleBy|_|) 3 9 + let _ = MyModule.(|DivisibleBy|_|) 3 9 + let _ = + match 9 with + | $$ 3 q -> q + | _ -> 0 + let _ = + match 9 with + | MyModule.$$ 3 q -> q + | _ -> 0 + """ + testCaseAsync "can get range of three-way total Active Pattern" + <| + // Three-way total active pattern: `(|Positive|Negative|Zero|)` + checkRanges + server + """ + module MyModule = + let ($D<|Posi$0tive|Negative|Zero|>D$) value = + if value > 0 then Positive + elif value < 0 then Negative + else Zero + + open MyModule + let _ = ($<|Positive|Negative|Zero|>$) 42 + let _ = MyModule.($<|Positive|Negative|Zero|>$) 42 + let _ = + match 42 with + | Positive -> 1 + | Negative -> -1 + | Zero -> 0 + let _ = + match 42 with + | MyModule.Positive -> 1 + | MyModule.Negative -> -1 + | MyModule.Zero -> 0 + """ + testCaseAsync "can get range of three-way total Active Pattern case (Positive)" + <| + // When clicking on one case of a three-way total active pattern + checkRanges + server + """ + module MyModule = + let (|$DD$|Negative|Zero|) value = + if value > 0 then $$ + elif value < 0 then Negative + else Zero + + open MyModule + let _ = (|Positive|Negative|Zero|) 42 + let _ = MyModule.(|Positive|Negative|Zero|) 42 + let _ = + match 42 with + | $$ -> 1 + | Negative -> -1 + | Zero -> 0 + let _ = + match 42 with + | MyModule.$$ -> 1 + | MyModule.Negative -> -1 + | MyModule.Zero -> 0 + """ + testCaseAsync "can get range of NonEmpty partial Active Pattern" + <| + // Partial active pattern for non-empty strings + checkRanges + server + """ + module MyModule = + let ($D<|Non$0Empty|_|>D$) (input: string) = + if System.String.IsNullOrWhiteSpace input then None + else Some input + + open MyModule + let _ = ($<|NonEmpty|_|>$) "test" + let _ = MyModule.($<|NonEmpty|_|>$) "test" + let _ = + match "test" with + | NonEmpty s -> s + | _ -> "" + let _ = + match "test" with + | MyModule.NonEmpty s -> s + | _ -> "" + """ + testCaseAsync "can get range of NonEmpty partial Active Pattern case" + <| + checkRanges + server + """ + module MyModule = + let (|$DD$|_|) (input: string) = + if System.String.IsNullOrWhiteSpace input then None + else Some input + + open MyModule + let _ = (|NonEmpty|_|) "test" + let _ = MyModule.(|NonEmpty|_|) "test" + let _ = + match "test" with + | $$ s -> s + | _ -> "" + let _ = + match "test" with + | MyModule.$$ s -> s + | _ -> "" + """ + testCaseAsync "can get range of Regex parameterized Active Pattern" + <| + // Parameterized active pattern for regex matching + checkRanges + server + """ + module MyModule = + let ($D<|Re$0gex|_|>D$) pattern input = + let m = System.Text.RegularExpressions.Regex.Match(input, pattern) + if m.Success then Some m.Value + else None + + open MyModule + let _ = ($<|Regex|_|>$) @"\d+" "abc123" + let _ = MyModule.($<|Regex|_|>$) @"\d+" "abc123" + let _ = + match "abc123" with + | Regex @"\d+" v -> v + | _ -> "" + let _ = + match "abc123" with + | MyModule.Regex @"\d+" v -> v + | _ -> "" + """ + testCaseAsync "can get range of Regex parameterized Active Pattern case" + <| + checkRanges + server + """ + module MyModule = + let (|$DD$|_|) pattern input = + let m = System.Text.RegularExpressions.Regex.Match(input, pattern) + if m.Success then Some m.Value + else None + + open MyModule + let _ = (|Regex|_|) @"\d+" "abc123" + let _ = MyModule.(|Regex|_|) @"\d+" "abc123" + let _ = + match "abc123" with + | $$ @"\d+" v -> v + | _ -> "" + let _ = + match "abc123" with + | MyModule.$$ @"\d+" v -> v + | _ -> "" + """ + testCaseAsync "can get range of inline struct partial Active Pattern - full pattern" + <| + // Inline struct partial active pattern - clicking on the full pattern (|StrStartsWith|_|) + // Tests that both function-call style usages like `(|StrStartsWith|_|) "hello" "world"` + // and match-case style usages like `| StrStartsWith "hello" ->` are found. + checkRanges + server + """ + module MyModule = + [] + let inline ($D<|StrSta$0rtsWith|_|>D$) (prefix: string) (item: string) = + if item.StartsWith prefix then ValueSome () else ValueNone + + // Function-call style usage in same module + let testDirect = ($<|StrStartsWith|_|>$) "hello" "hello world" + + open MyModule + // Function-call style usage with open + let _ = ($<|StrStartsWith|_|>$) "hello" "hello world" + // Function-call style usage with qualified name + let _ = MyModule.($<|StrStartsWith|_|>$) "hello" "hello world" + // Match-case style usage + let _ = + match "hello world" with + | StrStartsWith "hello" -> true + | _ -> false + let _ = + match "hello world" with + | MyModule.StrStartsWith "hello" -> true + | _ -> false + """ + testCaseAsync "can get range of inline struct partial Active Pattern case" + <| + // When clicking on the case name in an inline struct partial active pattern + // Only match-case style usages are found for the case (FCS limitation) + // Function-call style usages use the full pattern, not individual cases + checkRanges + server + """ + module MyModule = + [] + let inline (|$DD$|_|) (prefix: string) (item: string) = + if item.StartsWith prefix then ValueSome () else ValueNone + + // Function-call style usage - NOT marked because FCS doesn't find it for case symbols + let testDirect = (|StrStartsWith|_|) "hello" "hello world" + + open MyModule + // Function-call style usages - NOT marked + let _ = (|StrStartsWith|_|) "hello" "hello world" + let _ = MyModule.(|StrStartsWith|_|) "hello" "hello world" + // Match-case style usages - these ARE found + let _ = + match "hello world" with + | $$ "hello" -> true + | _ -> false + let _ = + match "hello world" with + | MyModule.$$ "hello" -> true + | _ -> false + """ testCaseAsync "can get range of type for static function call" <| checkRanges server @@ -633,6 +1086,7 @@ let tests state = "Find All References tests" [ scriptTests state solutionTests state + activePatternProjectTests state untitledTests state rangeTests state ] diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/ActivePatternProject.fsproj b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/ActivePatternProject.fsproj new file mode 100644 index 000000000..6ad865c5f --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/ActivePatternProject.fsproj @@ -0,0 +1,14 @@ + + + + net8.0 + + + + + + + + + + diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/Module1.fs b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/Module1.fs new file mode 100644 index 000000000..a77539c5e --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/Module1.fs @@ -0,0 +1,116 @@ +namespace ActivePatternProject + +/// First module that uses patterns from Patterns module +module Module1 = + open Patterns + + // Using total active pattern Even|Odd + let classifyNumber n = + match n with + | Even -> "even" + | Odd -> "odd" + + // Using total active pattern as function + let getEvenOdd n = (|Even|Odd|) n + + // Using partial active pattern ParseInt + let tryParseNumber input = + match input with + | ParseInt n -> Some n + | _ -> None + + // Using partial active pattern as function + let parseIntDirect input = (|ParseInt|_|) input + + // Using ParseFloat partial active pattern + let tryParseFloat input = + match input with + | ParseFloat f -> Some f + | _ -> None + + // Using ParseFloat as function + let parseFloatDirect input = (|ParseFloat|_|) input + + // Using parameterized active pattern + let isDivisibleBy3 n = + match n with + | DivisibleBy 3 result -> Some result + | _ -> None + + // Using multiple patterns in one match + let analyzeNumber n = + match n with + | Even & Positive -> "even positive" + | Even & Negative -> "even negative" + | Odd & Positive -> "odd positive" + | Odd & Negative -> "odd negative" + | Zero -> "zero" + + // Using Positive|Negative|Zero pattern + let getSign n = + match n with + | Positive -> 1 + | Negative -> -1 + | Zero -> 0 + + // ============================================ + // STRUCT PARTIAL ACTIVE PATTERNS + // ============================================ + + // Using struct partial active pattern ParseIntStruct + let tryParseNumberStruct input = + match input with + | ParseIntStruct n -> ValueSome n + | _ -> ValueNone + + // Using struct partial active pattern as function + let parseIntStructDirect input = (|ParseIntStruct|_|) input + + // Using ParseFloatStruct partial active pattern + let tryParseFloatStruct input = + match input with + | ParseFloatStruct f -> ValueSome f + | _ -> ValueNone + + // Using ParseFloatStruct as function + let parseFloatStructDirect input = (|ParseFloatStruct|_|) input + + // Using NonEmptyStruct partial active pattern + let validateInputStruct input = + match input with + | NonEmptyStruct s -> ValueSome s + | _ -> ValueNone + + // Using struct parameterized active pattern + let isDivisibleBy3Struct n = + match n with + | DivisibleByStruct 3 result -> ValueSome result + | _ -> ValueNone + + // ============================================ + // INLINE GENERIC ACTIVE PATTERNS + // ============================================ + + // Using IsOneOfChoice - inline generic struct parameterized pattern + let checkIfStartsWithPrefix input = + match input with + | IsOneOfChoice ((|StrStartsWith|_|), ["hello"; "hi"; "hey"]) -> true +//> ^^^^^^^^^^^^^ IsOneOfChoice + | _ -> false + + // Using IsOneOfChoice as a function + let checkPrefixDirect input = + (|IsOneOfChoice|_|) ((|StrStartsWith|_|), ["hello"; "hi"]) input +//> ^^^^^^^^^^^^^^^^^ IsOneOfChoice + + // Using StrStartsWithOneOf which uses IsOneOfChoice internally + let checkGreeting input = + match input with + | StrStartsWithOneOf ["hello"; "hi"; "hey"] -> "greeting" + | _ -> "not a greeting" + + // Using StrStartsWith directly + let startsWithHello input = + match input with + | StrStartsWith "hello" -> true + | _ -> false diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/Module2.fs b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/Module2.fs new file mode 100644 index 000000000..2d184e030 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/Module2.fs @@ -0,0 +1,109 @@ +namespace ActivePatternProject + +/// Second module - uses patterns with qualified access +module Module2 = + + // Using patterns with fully qualified names + let classifyWithQualified n = + match n with + | Patterns.Even -> "even" + | Patterns.Odd -> "odd" + + // Using partial pattern with qualified name + let parseWithQualified input = + match input with + | Patterns.ParseInt n -> Some n + | _ -> None + + // Using the pattern as a function with qualified name + let parseIntQualified input = Patterns.(|ParseInt|_|) input + let evenOddQualified n = Patterns.(|Even|Odd|) n + + // Using Regex pattern + let matchEmail input = + match input with + | Patterns.Regex @"[\w.]+@[\w.]+" email -> Some email + | _ -> None + + // Using NonEmpty pattern + let validateInput input = + match input with + | Patterns.NonEmpty s -> Ok s + | _ -> Error "Input cannot be empty" + + // Complex example with multiple patterns + let processInput input = + match input with + | Patterns.NonEmpty s -> + match s with + | Patterns.ParseInt n -> + match n with + | Patterns.Even -> "parsed even number" + | Patterns.Odd -> "parsed odd number" + | Patterns.ParseFloat f -> sprintf "parsed float: %f" f + | _ -> "non-numeric string" + | _ -> "empty input" + + // Using DivisibleBy with different parameters + let checkDivisibility n = + match n with + | Patterns.DivisibleBy 2 _ -> "divisible by 2" + | Patterns.DivisibleBy 3 _ -> "divisible by 3" + | Patterns.DivisibleBy 5 _ -> "divisible by 5" + | _ -> "not divisible by 2, 3, or 5" + + // ============================================ + // STRUCT PARTIAL ACTIVE PATTERNS (qualified access) + // ============================================ + + // Using struct partial pattern with qualified name + let parseWithQualifiedStruct input = + match input with + | Patterns.ParseIntStruct n -> ValueSome n + | _ -> ValueNone + + // Using struct pattern as a function with qualified name + let parseIntStructQualified input = Patterns.(|ParseIntStruct|_|) input + let parseFloatStructQualified input = Patterns.(|ParseFloatStruct|_|) input + + // Complex example with struct patterns + let processInputStruct input = + match input with + | Patterns.NonEmptyStruct s -> + match s with + | Patterns.ParseIntStruct n -> + match n with + | Patterns.Even -> "parsed even number (struct)" + | Patterns.Odd -> "parsed odd number (struct)" + | Patterns.ParseFloatStruct f -> sprintf "parsed float (struct): %f" f + | _ -> "non-numeric string" + | _ -> "empty input" + + // Using struct DivisibleBy with different parameters + let checkDivisibilityStruct n = + match n with + | Patterns.DivisibleByStruct 2 _ -> "divisible by 2 (struct)" + | Patterns.DivisibleByStruct 3 _ -> "divisible by 3 (struct)" + | Patterns.DivisibleByStruct 5 _ -> "divisible by 5 (struct)" + | _ -> "not divisible by 2, 3, or 5" + + // ============================================ + // INLINE GENERIC ACTIVE PATTERNS (qualified access) + // ============================================ + + // Using IsOneOfChoice as a function with qualified access + let checkPrefixQualified input = + Patterns.(|IsOneOfChoice|_|) (Patterns.(|StrStartsWith|_|), ["hello"; "hi"]) input +//> ^^^^^^^^^^^^^^^^^ IsOneOfChoice + + // Using StrStartsWithOneOf with qualified access + let checkGreetingQualified input = + match input with + | Patterns.StrStartsWithOneOf ["hello"; "hi"; "hey"] -> "greeting" + | _ -> "not a greeting" + + // Using StrStartsWith with qualified access + let startsWithHelloQualified input = + match input with + | Patterns.StrStartsWith "hello" -> true + | _ -> false diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/Patterns.fs b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/Patterns.fs new file mode 100644 index 000000000..9790a6937 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/Patterns.fs @@ -0,0 +1,109 @@ +namespace ActivePatternProject + +module Seq = + let inline tryPickV chooser (source: seq<'T>) = + use e = source.GetEnumerator() + let mutable res = ValueNone + while (ValueOption.isNone res && e.MoveNext()) do + res <- chooser e.Current + res + +/// Module containing various active pattern definitions +module Patterns = + + // ============================================ + // TOTAL/FULL ACTIVE PATTERNS + // ============================================ + + /// Total active pattern for even/odd classification + let (|Even|Odd|) value = + if value % 2 = 0 then Even else Odd + + /// Total active pattern for sign classification + let (|Positive|Negative|Zero|) value = + if value > 0 then Positive + elif value < 0 then Negative + else Zero + + // ============================================ + // PARTIAL ACTIVE PATTERNS + // ============================================ + + /// Partial active pattern for parsing integers + let (|ParseInt|_|) (input: string) = + match System.Int32.TryParse input with + | true, v -> Some v + | false, _ -> None + + /// Partial active pattern for parsing floats + let (|ParseFloat|_|) (input: string) = + match System.Double.TryParse input with + | true, v -> Some v + | false, _ -> None + + /// Partial active pattern for non-empty strings + let (|NonEmpty|_|) (input: string) = + if System.String.IsNullOrWhiteSpace input then None + else Some input + + // ============================================ + // PARAMETERIZED ACTIVE PATTERNS + // ============================================ + + /// Parameterized active pattern for divisibility + let (|DivisibleBy|_|) divisor value = + if value % divisor = 0 then Some(value / divisor) + else None + + /// Parameterized active pattern for regex matching + let (|Regex|_|) pattern input = + let m = System.Text.RegularExpressions.Regex.Match(input, pattern) + if m.Success then Some m.Value + else None + + // ============================================ + // STRUCT PARTIAL ACTIVE PATTERNS (F# 7+) + // These use ValueOption for better performance (no heap allocation) + // ============================================ + + /// Struct partial active pattern for parsing integers + [] + let (|ParseIntStruct|_|) (input: string) = + match System.Int32.TryParse input with + | true, v -> ValueSome v + | false, _ -> ValueNone + + /// Struct partial active pattern for parsing floats + [] + let (|ParseFloatStruct|_|) (input: string) = + match System.Double.TryParse input with + | true, v -> ValueSome v + | false, _ -> ValueNone + + /// Struct partial active pattern for non-empty strings + [] + let (|NonEmptyStruct|_|) (input: string) = + if System.String.IsNullOrWhiteSpace input then ValueNone + else ValueSome input + + /// Struct parameterized active pattern for divisibility + [] + let inline (|DivisibleByStruct|_|) divisor value = + if value % divisor = 0 then ValueSome(value / divisor) + else ValueNone + + + [] + let inline (|IsOneOfChoice|_|) (chooser: 'a -> 'b -> 'c voption, values : 'a seq) (item : 'b) = +//> ^^^^^^^^^^^^^^^^^ IsOneOfChoice + values |> Seq.tryPickV (fun x -> chooser x item) + + [] + let inline (|StrStartsWith|_|) (value : string) (item : string) = + if item.StartsWith value then ValueSome () + else ValueNone + + [] + let inline (|StrStartsWithOneOf|_|) (values : string seq) (item : string) = + (|IsOneOfChoice|_|) ((|StrStartsWith|_|), values) item +//> ^^^^^^^^^^^^^^^^^ IsOneOfChoice diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/Program.fs b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/Program.fs new file mode 100644 index 000000000..c609ed4ce --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/ActivePatternProject/Program.fs @@ -0,0 +1,107 @@ +namespace ActivePatternProject + +/// Main program that uses patterns from all modules +module Program = + open Patterns + open Module1 + open Module2 + + // Direct usage of patterns + let demo1 () = + // Even|Odd usage + let result = + match 42 with + | Even -> "forty-two is even" + | Odd -> "forty-two is odd" + printfn "%s" result + + let demo2 () = + // ParseInt usage + match "123" with + | ParseInt n -> printfn "Parsed: %d" n + | _ -> printfn "Failed to parse" + + let demo3 () = + // Using patterns as functions + let evenOddResult = (|Even|Odd|) 100 + let parseResult = (|ParseInt|_|) "456" + printfn "EvenOdd: %A, Parse: %A" evenOddResult parseResult + + let demo4 () = + // Cross-module usage + let m1Result = classifyNumber 10 + let m2Result = classifyWithQualified 20 + printfn "Module1: %s, Module2: %s" m1Result m2Result + + let demo5 () = + // Positive|Negative|Zero usage + let values = [-5; 0; 5] + for v in values do + match v with + | Positive -> printfn "%d is positive" v + | Negative -> printfn "%d is negative" v + | Zero -> printfn "%d is zero" v + + let demo6 () = + // ParseFloat usage + match "3.14" with + | ParseFloat f -> printfn "Float: %f" f + | _ -> printfn "Not a float" + + let demo7 () = + // DivisibleBy usage + for n in 1..15 do + match n with + | DivisibleBy 3 q -> printfn "%d / 3 = %d" n q + | _ -> () + + // ============================================ + // STRUCT PARTIAL ACTIVE PATTERNS demos + // ============================================ + + let demoStruct1 () = + // ParseIntStruct usage + match "789" with + | ParseIntStruct n -> printfn "Parsed (struct): %d" n + | _ -> printfn "Failed to parse" + + let demoStruct2 () = + // ParseFloatStruct usage + match "2.718" with + | ParseFloatStruct f -> printfn "Float (struct): %f" f + | _ -> printfn "Not a float" + + let demoStruct3 () = + // Using struct patterns as functions + let parseIntResult = (|ParseIntStruct|_|) "321" + let parseFloatResult = (|ParseFloatStruct|_|) "1.618" + printfn "ParseInt (struct): %A, ParseFloat (struct): %A" parseIntResult parseFloatResult + + let demoStruct4 () = + // NonEmptyStruct usage + match "hello" with + | NonEmptyStruct s -> printfn "Non-empty (struct): %s" s + | _ -> printfn "Empty string" + + let demoStruct5 () = + // DivisibleByStruct usage + for n in 1..15 do + match n with + | DivisibleByStruct 5 q -> printfn "%d / 5 = %d (struct)" n q + | _ -> () + + [] + let main _ = + demo1 () + demo2 () + demo3 () + demo4 () + demo5 () + demo6 () + demo7 () + demoStruct1 () + demoStruct2 () + demoStruct3 () + demoStruct4 () + demoStruct5 () + 0 diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/Solution/B/MyModule3.fs b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/Solution/B/MyModule3.fs index b93543735..ba170d8d4 100644 --- a/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/Solution/B/MyModule3.fs +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/Solution/B/MyModule3.fs @@ -16,4 +16,34 @@ let _ = doStuff () let _ = internalValue + 42 //> ^^^^^^^^^^^^^ internal value let _ = 2 * internalValue + 42 -//> ^^^^^^^^^^^^^ internal value \ No newline at end of file +//> ^^^^^^^^^^^^^ internal value + +// Using active patterns from WorkingModule + +// Total active pattern usage +let _ = (|Even|Odd|) 100 +let classifyInModule3 n = + match n with + | Even -> "even" + | Odd -> "odd" + +// Partial active pattern usage (cross-file) +// NOTE: No markers here - see WorkingModule.fs for explanation of FCS limitations +let _ = (|ParseInt|_|) "999" +let parseInModule3 input = + match input with + | ParseInt n -> Some n + | _ -> None + +// ============================================ +// INLINE ACTIVE PATTERN CROSS-FILE USAGES +// NOTE: No markers - see B/WorkingModule.fs for explanation of FCS limitations +// ============================================ + +// Function-call style usage cross-file +let _ = (|StrPrefix|_|) "hi" "hi there" +// Match-case style usage cross-file +let checkPrefix input = + match input with + | StrPrefix "test" -> true + | _ -> false \ No newline at end of file diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/Solution/B/WorkingModule.fs b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/Solution/B/WorkingModule.fs index 0782f5870..4a0dc7622 100644 --- a/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/Solution/B/WorkingModule.fs +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/Solution/B/WorkingModule.fs @@ -63,3 +63,55 @@ let _ = B.MyModule1.value //> ^^^^^ function from same project let _ = B.MyModule1.``value`` //> ^^^^^^^^^ function from same project + +// ============================================ +// ACTIVE PATTERNS +// ============================================ + +// Total active pattern: Even|Odd +// Cross-file references work for BackgroundCompiler +// TransparentCompiler has issues (tracked separately) +let (|Even|Odd|) value = + if value % 2 = 0 then Even else Odd + +// Testing full pattern as function +let _ = (|Even|Odd|) 42 +let _ = + match 42 with + | Even -> "even" + | Odd -> "odd" + +// Partial active pattern: ParseInt +// NOTE: Cross-file Find All References for active patterns has FCS limitations: +// - FCS doesn't find cross-file match-case usages for active pattern cases +// - TransparentCompiler has additional issues with cross-file references +let (|ParseInt|_|) (input: string) = + match System.Int32.TryParse input with + | true, v -> Some v + | false, _ -> None + +// Testing partial pattern as function +let _ = (|ParseInt|_|) "42" +let _ = + match "123" with + | ParseInt n -> n + | _ -> 0 + +// ============================================ +// INLINE STRUCT PARTIAL ACTIVE PATTERNS +// NOTE: Cross-file references for inline active patterns have FCS limitations +// See comments in Partial active pattern section above +// ============================================ + +/// Inline struct partial active pattern for string prefix matching +[] +let inline (|StrPrefix|_|) (prefix: string) (item: string) = + if item.StartsWith prefix then ValueSome () else ValueNone + +// Function-call style usage in same module +let _ = (|StrPrefix|_|) "hello" "hello world" +// Match-case style usage in same module +let _ = + match "hello world" with + | StrPrefix "hello" -> true + | _ -> false diff --git a/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/Solution/C/MyModule1.fs b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/Solution/C/MyModule1.fs index f7955cab6..ac244ecc3 100644 --- a/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/Solution/C/MyModule1.fs +++ b/test/FsAutoComplete.Tests.Lsp/TestCases/FindReferences/Solution/C/MyModule1.fs @@ -12,3 +12,34 @@ let _ = B.WorkingModule.doStuff () open B.WorkingModule let _ = doStuff () //> ^^^^^^^ public function + +// Using active patterns from B.WorkingModule (cross-project) + +// Total active pattern usage (qualified) +let _ = B.WorkingModule.(|Even|Odd|) 200 +let classifyInC n = + match n with + | B.WorkingModule.Even -> "even" + | B.WorkingModule.Odd -> "odd" + +// Total active pattern usage (open) +let _ = (|Even|Odd|) 300 +let classifyInCOpen n = + match n with + | Even -> "even" + | Odd -> "odd" + +// Partial active pattern usage (qualified) - cross-file +// NOTE: No markers here - see B/WorkingModule.fs for explanation of FCS limitations +let _ = B.WorkingModule.(|ParseInt|_|) "777" +let parseInCQualified input = + match input with + | B.WorkingModule.ParseInt n -> Some n + | _ -> None + +// Partial active pattern usage (open) - cross-file +let _ = (|ParseInt|_|) "888" +let parseInCOpen input = + match input with + | ParseInt n -> Some n + | _ -> None