Skip to content

Conversation

@kerams
Copy link
Contributor

@kerams kerams commented Apr 14, 2022

Implements #6996.

When it's determined we're pattern matching against a DU of a specific type, the completions will only show the latter's cases as well as compatible active patterns in scope.

l4O3LHWDly

We could go further:

let call (choice: Choice) =
    match choice with
    | Choice1 ((), C$

Here we can tell that we're pattern matching on the second argument of Choice1, which is of type Choice, so completions should be filtered in the same fashion again. But that's for another time ;).

let eventInfoCache = MakeInfoCache GetIntrinsicEventInfosUncached hashFlags1
let namedItemsCache = MakeInfoCache GetIntrinsicNamedItemsUncached hashFlags2
let mostSpecificOverrideMethodInfoCache = MakeInfoCache GetIntrinsicMostSpecificOverrideMethodSetsUncached hashFlags0
let unionCaseInfoCache = MakeInfoCache GetIntrinsicUnionCaseInfosUncached hashFlags3
Copy link
Contributor Author

@kerams kerams Apr 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cache seems to be invalidated every time I make a change in the document (the DU stays the same though), so perhaps it's completely unnecessary? Or perhaps it's supposed to work for unions external to the current document?

@kerams kerams marked this pull request as ready for review April 20, 2022 20:00
@kerams kerams marked this pull request as draft April 20, 2022 20:23
@auduchinok
Copy link
Member

@kerams Could you please add tests for RequireQualifiedAccess unions and for out-of-scope unions?

@kerams
Copy link
Contributor Author

kerams commented Apr 22, 2022

I'm really pleased with the outcome. DU type is added automatically when qualified access is required. match! and function application both work now:

bang

When the cases are not in scope, the appropriate namespace will be opened on commit:

unres

When we're matching against an unknown type, only items that are legal in a match clause pattern are shown, namely literals, enums, union (cases), exception cases, active patterns, modules and namespaces:

Gs8nIxbRDd

Union cases, among other completion items, unfortunately fall under CompletionItemKind.Other, so we cannot adjust the priorities that @Happypig375 called for in the original issue without making a breaking change.

Marking it as ready for review. Tests will be added once the other completion PRs are merged in order to avoid conflicts.

@kerams kerams marked this pull request as ready for review April 22, 2022 16:57
@kerams
Copy link
Contributor Author

kerams commented Apr 22, 2022

#5594 should be resolved too.

@vzarytovskii
Copy link
Member

vzarytovskii commented Apr 29, 2022

Will it be possible to add some simple tests (including what @auduchinok suggested) please? Otherwise looks good to me.

@kerams
Copy link
Contributor Author

kerams commented Apr 29, 2022

Of course, after the other completion PRs get in.

@vzarytovskii
Copy link
Member

Of course, after the other completion PRs get in.

Awesome, thanks. You mean #12933? Just merged it. Let me know if you'll need some help with conflicts.

@kerams
Copy link
Contributor Author

kerams commented Apr 29, 2022

Ideally #12837, #12873, #12930. #12872 also seems to be ready to be reviewed.

@vzarytovskii
Copy link
Member

Ideally #12837, #12873, #12930. #12872 also seems to be ready to be reviewed.

I've merged most of them, last one is still passing CI after merge.

@kerams
Copy link
Contributor Author

kerams commented Apr 29, 2022

@vzarytovskii, @dsyme, @KevinRansom, while I'm adding more tests, can you give this a go? It's a major change (towards the better, with any luck), so I'd like to know everything behaves as expected and feels right to others too.

There are further low-hanging fruits that I'm leaving open for followup PRs (right now these positions should give you the full completions list)

  • tuples
     match Some 5, Some 5 with
     | z, y$
  • array
    match x with
    | [||] -> ()
    | a$
  • array/list elements
    match [ Some 4 ] with
    | [ e$ ]

@dsyme
Copy link
Contributor

dsyme commented May 1, 2022

@kerams I'll take a close look at this and give it a go

Some initial questions I'll be thinking about:

  • Do you also show type, module and namespace names (e.g. that eventually contain potential completions)?
  • Are AutoOpen and RequireQualifiedAccess sufficiently taken into account?

@kerams
Copy link
Contributor Author

kerams commented May 1, 2022

Do you also show type, module and namespace names (e.g. that eventually contain potential completions)?

In general yes. However, when it's known (via type annotations or previous match clauses) that the type of the identifier the user is completing is a DU, only its cases and active patterns are shown.

Are AutoOpen and RequireQualifiedAccess sufficiently taken into account?

RQA should be covered. AutoOpen works like this

devenv_f2olQhktgt

devenv_dX5aNrE618

In the second case open N.M will be added on commit. The .M bit is reduntant, but I don't know how difficult it would be to remove it. Right now I just use uci.Tycon.PublicPath |> Option.map (fun x -> x.EnclosingPath), which as I have just now found out is problematic

devenv_HoRXNuC83r

This opens F.N.M which does not compile. Adding test cases for this and I'll need to figure out how to strip the path to open to the bare minimum.

@kerams
Copy link
Contributor Author

kerams commented May 1, 2022

Got it, trimPathByDisplayEnv denv uci.Tycon.CompilationPath.DemangledPath instead of uci.Tycon.PublicPath |> Option.map (fun x -> x.EnclosingPath). This does not solve the minor .M issue, since the path is still just a string list here and I don't see an immediate way of reconciling AutoOpen with that.

@kerams
Copy link
Contributor Author

kerams commented May 1, 2022

One more thing

module N =
    module M =
        type ChoiceZ =
            | Choice1
            | Choice2

open N

let call (choice: N.M.ChoiceZ) =
    match choice with
    | c

inserts the open at the wrong place

module N =
    module M =
        type ChoiceZ =
            | Choice1
            | Choice2

open M

open N

let call (choice: N.M.ChoiceZ) =
    match choice with
    | Choice1

I'm inclined to say that correct open insertion is not my problem :).

@vzarytovskii
Copy link
Member

vzarytovskii commented May 3, 2022

I'm inclined to say that correct open insertion is not my problem :)

As long as it inserts a correct open (i.e. syntactically and semantically), it's fine, if needed, auto-inserts of open statements can be fixed as a separate issue/pr.

@dsyme
Copy link
Contributor

dsyme commented May 16, 2022

Hmmm stack overflow failure in test on MacOS

The active test run was aborted. Reason: Test host process crashed : Stack overflow.
   at Microsoft.FSharp.Collections.MapTreeModule.mk[[System.__Canon, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.__Canon, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](Microsoft.FSharp.Collections.MapTree`2<System.__Canon,System.__Canon>, System.__Canon, System.__Canon, Microsoft.FSharp.Collections.MapTree`2<System.__Canon,System.__Canon>)
   at Microsoft.FSharp.Collections.MapTreeModule.add[[System.__Canon, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.__Canon, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](System.Collections.Generic.IComparer`1<System.__Canon>, System.__Canon, System.__Canon, Microsoft.FSharp.Collections.MapTree`2<System.__Canon,System.__Canon>)
   at Microsoft.FSharp.Collections.MapTreeModule.add[[System.__Canon, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.__Canon, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](System.Collections.Generic.IComparer`1<System.__Canon>, System.__Canon, System.__Canon, Microsoft.FSharp.Collections.MapTree`2<System.__Canon,System.__Canon>)
   at Microsoft.FSharp.Collections.MapTreeModule.add[[System.__Canon, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.__Canon, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](System.Collections.Generic.IComparer`1<System.__Canon>, System.__Canon, System.__Canon, Microsoft.FSharp.Collections.MapTree`2<System.__Canon,System.__Canon>)
   at Microsoft.FSharp.Collections.FSharpMap`2[[System.__Canon, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.__Canon, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].Add(System.__Canon, System.__Canon)
   at Microsoft.FSharp.Collections.ListModule.FoldBack[[System.__Canon, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.__Canon, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](Microsoft.FSharp.Core.FSharpFunc`2<System.__Canon,Microsoft.FSharp.Core.FSharpFunc`2<System.__Canon,System.__Canon>>, Microsoft.FSharp.Collections.FSharpList`1<System.__Canon>, System.__Canon)
   at FSharp.Compiler.TypedTree+ModuleOrNamespaceType.get_AllValsAndMembersByPartialLinkageKey()
   at FSharp.Compiler.TypedTree+ModuleOrNamespaceType.TryLinkVal(CcuThunk, ValLinkageFullKey)
   at FSharp.Compiler.TypedTree+ValRef.get_TryDeref()
   at FSharp.Compiler.TypedTreeBasics.primValRefEq(Boolean, CcuThunk, ValRef, ValRef)
   at FSharp.Compiler.TypedTreeOps.|ValApp|_|(TcGlobals, ValRef, Expr)
   at FSharp.Compiler.LowerComputedCollectionExpressions.|SeqToList|_|(TcGlobals, Expr)
   at FSharp.Compiler.LowerComputedCollectionExpressions.LowerComputedListOrArrayExpr(Microsoft.FSharp.Core.FSharpFunc`2<ValRef,Microsoft.FSharp.Core.FSharpFunc`2<ValUseFlag,Microsoft.FSharp.Core.FSharpFunc`2<Microsoft.FSharp.Collections.FSharpList`1<TType>,Microsoft.FSharp.Core.FSharpFunc`2<FSharp.Compiler.Text.Range,System.Tuple`2<Expr,TType>>>>>, TcGlobals, ImportMap, Expr)
   at FSharp.Compiler.IlxGen.GenExprPreSteps(cenv, CodeGenBuffer, IlxGenEnv, Expr, sequel)
   at FSharp.Compiler.IlxGen.GenExprAux(cenv, CodeGenBuffer, IlxGenEnv, Expr, sequel)
   at [email protected](Microsoft.FSharp.Core.Unit)

@dsyme
Copy link
Contributor

dsyme commented May 16, 2022

I'm doing some adhoc testing for completions in pattern matching and will jot notes on various cases I've tried using this PR.

🟢 Manually verified fixed as part of this PR
🟠 Ideally needs to be fixed but probably not part of this PR
🔴 Should probably be addressed as part of this PR


🟠 Completions are not restricted here. This is likely a more general problem in parser error recovery. We should get this fixed because it's so basic, but I'm not expecting it to be part of this PR

open System

let f n =   
    match n with                                    
    | Choice3Of3 _ ->
        Console.WriteLine("C")
    |  $

🟠 Likewise completions are not restricted here (ideally they should be). This is likely the same problem in parser error recovery.

open System

type C = AAAA | BBB
let f (n: C) =   
    match n with                                    
    | $

🟢 They are filtered here 😃 😃 😃 😃

open System

type C = AAAA | BBB | CCC

let f (n: C) =   
    match n with                                    
    | C$
    | BBB -> 2

🔴 Over-filtering - module Cat not included here - we expect it to be shown - this has been discussed above and below

open System

type C = AAAA | BBB | CCC

module Cat =
    type D = FFF | GGG
    let (|HHH|_|) (c: C) = Some 1

let f (n: C) =   
    match n with                                    
    | C$
    | BBB -> 2

🟢 Correct filtering here:

type C = AAAA | BBB | CCC

let f (n: C) =   
    match n with                                    
    | (C$)
    | BBB -> 2

🟠 No filtering here, because no captured resolutions, we could improve this in a separate PR

type C = AAAA | BBB | CCC

let f (n: C) =   
    match n with                                    
    | C$ _ -> 1
    | BBB -> 2

🔴 No HHH available here, though filtering has occured:

type C = AAAA | BBB | CCC

let (|HHH|) (c: C) = None

let f (n: C) =   
    match n with                                    
    | H$
    | BBB -> 2

Likewise here:

type C = AAAA | BBB | CCC

let (|HHH|_|) (c: C) = None

let f (n: C) =   
    match n with                                    
    | H$
    | BBB -> 2

Likewise here:

type C = AAAA | BBB | CCC

let (|HHH|JJJ|) (c: C) = HHH

let f (n: C) =   
    match n with                                    
    | H
    | BBB -> 2

I was also expecting this to filter?

type C() = class end

let (|HHH|JJJ|) (c: C) = HHH

let f (n: C) =   
    match n with                                    
    | H
    | _ -> 2

@dsyme
Copy link
Contributor

dsyme commented May 16, 2022

I'd be loath to clutter the completion list with things that are only relevant in the rare cases when someone wants to use a qualified active pattern name instead of opening its path.

As a general rule, all valid completions must always be shown - if a valid completion is not in the list the user will generally consider this a bug in auto-complete. There are a couple of places we violate this rule (e.g. we don't show op_Addition or get_Name as a completion etc.) but these are usually deliberate and associated with non-preferred was of coding something.

I think we need to filter the modules according to whether they have contain relevant active patterns or not. I'll take a look to see how feasible that is. Searching inside modules will generally be OK from a performance point of view, especially if cached in some way, though we need to take a little care.

I believe that without the aforementioned ordering issues taken care of, this would really detract from the perceived benefit of the filtering.

I do agree with this - though I think we should first try hard to stick to the principle of showing all valid completions

@auduchinok
Copy link
Member

I think we need to filter the modules according to whether they have contain relevant active patterns or not. I'll take a look to see how feasible that is. Searching inside modules will generally be OK from a performance point of view, especially if cached in some way, though we need to take a little care.

It would force reading nested modules and namespaces that may sometimes be unwanted. @dsyme Could you make this behaviour optional if you're going to work on it, please?

Copy link
Contributor

@dsyme dsyme left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked over again and left some comments

EntityRefContainsSomethingAccessible ncenv m ad modref &&
not (IsTyconUnseen ad g ncenv.amap m modref))
|> List.map ItemForModuleOrNamespaceRef
GetVisibleNamespacesAndModulesAtPoint ncenv nenv fullyQualified m ad |> List.map ItemForModuleOrNamespaceRef
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for factoring this out. There's quite a lot of duplication in NameResolution.fs ResolvePartial* routines and it would be really good to remove all inner functions and factor out all common/reusable predicate code

| TType_var (typar, _) ->
match typar.Solution with
| Some paramType ->
doesActivePatternTakeTypeAsInput g ty paramType
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use this I believe:

                    if TypeRelations.TypeFeasiblySubsumesType 0 g amap m thisTy TypeRelations.CanCoerce ty then

| TType_var (typar, _) ->
match typar.Solution with
| Some paramType ->
doesActivePatternTakeTypeAsInput g ty paramType
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we ideally want to filter active patterns as well as we filter union cases. It's not good long-term if tooling works better with union cases than active patterns as it encourages people to rely on the union cases (e.g. not hide them) and avoid using active patterns.

@kerams
Copy link
Contributor Author

kerams commented May 16, 2022

@dsyme, so I've stepped through this

No filtering here, I think we could expect this to be filtered as part of this PR?

type C = AAAA | BBB | CCC

let f (n: C) =   
    match n with                                    
    | C$ _ -> 1
    | BBB -> 2

The problem is the fact that captured name resolutions do not contain anything for that line. Not sure why that is.

@dsyme
Copy link
Contributor

dsyme commented May 16, 2022

It would force reading nested modules and namespaces that may sometimes be unwanted. @dsyme Could you make this behaviour optional if you're going to work on it, please?

Filtering namespaces is not really possible because it would eagerly explore too much metadata. This means we either have to show all available namespaces, or break the "show all completions" design principle in this case. I think it's a reasonable place to break this rule. Note we could turn off filtering for A.B$ completions too, so SomeNamespace.$ would no longer filter. That seems ok as the SomeNamespace acts as such a strong filer anyway?

For modules I do think we should do the the work to look in the contents of the module for relevant active patterns as part of this PR (we could also include all module names for now and do it in a separate PR). Reasons:

  1. open Quotations then BasicPatterns.XYZ is a pretty standard way of writing pattern matching code for quotations
  2. we don't want tooling that discourages the uses of modules to organize a collections of active pattern names.

Reading through nested modules is feasible, as F# metadata is always read eagerly. We should however cache the computation result in InfoReader.

I don't think we'd make it optional - we don't currently allow user-specified options for auto-complete and the spec of auto-complete is pretty plain: show all available, legitimate completions, and preferably no more (with some deliberate exceptions).

@dsyme
Copy link
Contributor

dsyme commented May 16, 2022

The problem is the fact that captured name resolutions do not contain anything for that line. Not sure why that is.

OK, I see. We can deal with these in follow up PRs if you prefer? I think the module name completions are the only blocking thing I've seen so far - also I'd like you to consider this comment https://github.com/dotnet/fsharp/pull/12990/files/5e285f23cb0d39011ebf278984400fb7e9af6a52#diff-da6f3313031f72e650eeae86b874d89513fe4b05a1fd6ebc5983a4415521d755

I've changed this one to 🟠 in the list above.

@auduchinok
Copy link
Member

Filtering namespaces is not really possible because it would eagerly explore too much metadata.

Yes, this is what I was afraid of happening. 🙂

@dsyme
Copy link
Contributor

dsyme commented May 16, 2022

Aside: In the far distant past (F# 0.9!) we did filter union case completions for pattern matching. However we did it imperfectly, and ended up ripping out all the code and ditching it. I don't want that cycle to repeat - if possible I'd like us to iterate to "get this right, and get it fully tested and pinned down" so the feature sticks for the long term. I can see that will take multiple PRs.

@kerams
Copy link
Contributor Author

kerams commented May 16, 2022

I was also expecting this to filter?
type C() = class end

C is not a DU, so it falls down into the general case, which you're saying should not be a separate case. It's mostly just values and keywords that are removed.

The rest of 🔴 should now be green.

I think we ideally want to filter active patterns as well as we filter union cases.

Can you please explain what you mean here? I'm not following.

@dsyme
Copy link
Contributor

dsyme commented May 16, 2022

I think we ideally want to filter active patterns as well as we filter union cases.

Can you please explain what you mean here? I'm not following.

To me the code should follow this spec:

If we're at a match identifier position for ID$ (or plain empty $)

  1. Keep union cases which are feasibly compatible with the known input type
  2. Keep active patterns whose input-position type is feasibly compatible with the known input type
  3. Keep literals whose type is feasibly compatible with the known input type
  4. Keep modules that contain at least one of these, perhaps recursively
  5. Keep union and enum type names where the type is feasibly compatible with the known input type
  6. Keep F# exception names if the input type is feasibly compatible with System.Exception.
  7. Don't keep namespaces - by design deliberately breaking the "all legitimate completions" principle.

So whether the input type is a union type is not particularly important except that part (1) is equivalent to checking if the input type is a union type and returning precisely the set of union cases supported by that union type. But even if the type is a union type (2), (4), (5) still apply . So structurally we may as well just always do (1) - (6), and that will have the benefit of correctly filtering such things as string literals (only if the type is feasibly compatible with 'string'), enum type names and so on.

@kerams
Copy link
Contributor Author

kerams commented May 16, 2022

I see, thanks.

So whether the input type is a union type is not particularly important except that part (1) is equivalent to checking if the input type is a union type and returning precisely the set of union cases supported by that union type.

That is true until the input type is a DU/enum that is out of scope. The relevant items then aren't in the original list in the first place. This is thus something that needs to be explicitly checked for every time, so that we can append additional completion items that open a module/namespace on commit. (Or not, it would not be a regression, but my current implementation is able to do this)

Do you want to have this approach to filtering cleaned up and sorted in this PR?

@auduchinok
Copy link
Member

auduchinok commented May 16, 2022

So whether the input type is a union type is not particularly important except that part (1) is equivalent to checking if the input type is a union type and returning precisely the set of union cases supported by that union type.

This may be quite unexpected in cases where some code is being or may be rewritten.

Imagine that you're matching a function return like this:

match f () with
| {caret}

And, while writing a pattern, you decided to use a different type, not the one that the function returns. In R# we only move relevant symbols up and don't remove them by default (there's another filtering completion mode that uses another Ctrl+Space stroke or a different shortcut, but it's not implemented for F# yet). Moving things up and not removing makes it possible 1) to use a symbol with unexpected type and 2) to use a quick fix that would update expected type after completing the item (i.e. change the function return type in the example above).

Breaking this scenario will make it much harder to change things and I'd consider this a serious regression.

I've tried moving relevant union cases up in some simple scenarios in F# plugin, and it already significantly improved the completion experience:
Screenshot 2022-05-16 at 18 30 35

Please note how it doesn't block you from using a different type, since the completion is about helping you type any allowed things in. Though, someUnit could be removed as it wouldn't be resolved in the pattern anyway.

@kerams
Copy link
Contributor Author

kerams commented May 17, 2022

Moving things up and not removing makes it possible 1) to use a symbol with unexpected type and 2) to use a quick fix that would update expected type after completing the item (i.e. change the function return type in the example above).

Fair enough. However, that's another dimension to the problem with VS sorting I described #12990 (comment). Not only will sorting need to be implemented as completion context dependent, but moving certain DU cases up instead of removing the rest of them will require sorting to also be type dependent.

Not saying I have a problem with that and what you do in Rider, but it's a bit more work and I don't immediately have a good idea of what the implementation would involve and look like. (Nor is it something I frankly feel like diving into myself right now.)

@dsyme
Copy link
Contributor

dsyme commented May 18, 2022

Do you want to have this approach to filtering cleaned up and sorted in this PR?

I think I would prefer that - it seems to me it will make the code more uniform across all the cases we need to consider?

I know you've taken a few iterations on this - if you're running out of steam I could take a pass to help you get it across the line?

@dsyme
Copy link
Contributor

dsyme commented May 18, 2022

This may be quite unexpected in cases where some code is being or may be rewritten.

This is an important consideration. It is also actually another one of the reasons we removed the filtering I mentioned before (false filtering when input type has not yet been changed).

@kerams
Copy link
Contributor Author

kerams commented May 20, 2022

@dsyme, I've done some work here. Things that are illegal in a match clause (and namespaces, non-union/enum types) are removed from the list completely. Everything else is sorted according to your spec - compatible items are on the top, the rest is just moved to the bottom.

I've introduced MajorPriority on CompletionItem and the public DeclarationListItem. Admittedly, this makes sorting even messier, but it was the fastest way to reach the desired behavior without reimplementing the whole thing.

One casualty of the refactoring are union cases out of scope, which now do not make it on the list.

@auduchinok
Copy link
Member

Things that are illegal in a match clause (and namespaces, non-union/enum types) are removed from the list completely.

I still think removing namespaces might be a bad thing. @dsyme Could we have a switch for this logic, at least for the time being? Maybe a param to the completion items methods?

@kerams kerams requested a review from dsyme May 30, 2022 15:08
@kerams kerams closed this Feb 25, 2023
@kerams kerams deleted the match branch April 19, 2023 06:51
@T-Gro
Copy link
Member

T-Gro commented May 25, 2023

/run fantomas

@github-actions
Copy link
Contributor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

5 participants