New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve discoverability of code refactorings #35525
Comments
Tagging @heejaechang @jinujoseph @vatsalyaagrawal, who were involved in the internal discussions in this area. Tagging @sharwell - can we please bring this up in today's design meeting? Tagging @dotnet/roslyn-ide @CyrusNajmabadi for additional views/concerns that must be discussed at the design meeting. |
I am a fan of 1.iii. We always have a lot of concern about what we show to the user and right now that decision is made in each individual refactoring. If we allowed the refactoring to say "this is what I will operate on", we could implement a smarter system for determining which refactorings are the most important. It could use more information than just the current cursor or selection location. We could allow recently used, frequently used, etc... to influence which refactorings get the most visibility. |
Note that even before we get into any of the implementation of approaches in 1, we should discuss if we want to do 1. (Show additional actions in light bulb) or 2. (New UI) or both to improve discoverability. Thanks @JoeRobich, good points. I think there are merits and demerits in all approaches that we should discuss. Heejae suggested approach 1.iii. and sounds reasonsable to me. |
My primary concern is that we spent a lot of effort dealing with teh feedback of: I see too many items in the lightbulb, and so many of them aren't useful or aren't relevant to where i am right now. Why is there so much crap here that i have to deal with. I've very worried toward slipping back toward that place esp after all that effort went in. |
Why don't we write docs and list all our features and where they can be used? IntelliJ and Resharper does this. It's not clear to me why we don't just list somewhere (and update when new VS releases come out):
|
Note: i bring this up because i've heard several times in gitter people going: oh wait, VS has that feature? Nice! I wish i'd known that. I think we're pushing too hard on hte idea that these features need to be 'in your face'. I think it's critical that they're not. This certainly hurts discoverability, but i think it's not necessary to presume the feature has to be discoverable directly from the produce. It can be discoverable through other means. |
@CyrusNajmabadi I think we need to be very careful to not be in your face about things, but they way spans work for refactorings today is a real problem. Most of our refactoring placement is extremely finicky. Many times people have been told we have a We need to be careful to not swing the pendulum too far back in the direction of over-showing refactorings, but its clear what we have today doesn't work for the majority of people. Even if we tell them they are unlikely to discover our features. |
We can do better on docs, and some of that is happening in parallel, but I also don't think a blog post or reading docs.microsoft.com should be a requirement to use our features. Ideally you should be able to understand and use them without taking time out of your day to go searching for docs. |
Of course. I totally agree with thsi feedback. I even helped work on creating helpers for this sort of thing. For example: RefactoringSelectionIsValidAsync I think we absolutely should have public helpers here to allow the refactoring author specify the range they want their feature to be available, and do appropriate computations to make the experience good for the user. For example, with teh 'caret is on line' case, a normal refactoring should have been written to:
I personally just think we need to audit our features and have them stop all doing this sort of thing in an adhoc manner. We can write these simple helpers and improve the scenarios pretty simply. |
I personally disagree. I think it's actually quite reasonable to have docs for this sort of thing, or else you're held hostage to trying to make teh features discoverable through every means a user may try. Say we do any/all of the above. And a user still doesn't figure it out. Maybe they think "to convert 'for' to 'foreach' i'll just add |
I agree with that. However, my overall point is that this isn't something that requires some large investment. A dev could literally walk through most or all of our refactorings in a couple of hours and address the deficiencies here. It's not like we don't have experience with thsi. We've tweaked refactorings several times. For example, we discovered it was common users to do the following to select a bunch of members: class C
{
int foo;[|
int bar;
int baz;|]
} This originally didn't work because the start of the span was on a field that wasn't being selected. But we realized this was a desirable coding pattern and we made it work. I think we can do the same for all the refactorings (ideally with shared code for consistency) without a larger piece of work here (i.e. converting to analyzers). |
Basically, my suggestoin to start would be the very cheap: Option 4: Go audit and fix these problematic refactorings. While you're doing that, try to come up with a reasonable set of rules we can publish, ideally along with helper functions for this. See where this gets us. It's extremely cheap and likely effective. If we find out after this that it's still insufficient, and even with these changes and with docs, that people can't figure it out then we invest in a more dramatic solution. |
I feel this is the crux of the problem here. We expect to throw different selection spans or cursor position at the refactorings, and expect each refactoring to decide if the refactoring is applicable based on the context or selection. It seems like all this should be the job of the code refactoring engine, not individual refactoring. Refactoring implementation should be exactly identical to a how a code fix would be implemented - just do FindToken or FindNode on input span and bail out if the found token/node is not one that it refactors. The engine should be the one who decides if the registered refactoring is the most applicable refactoring based on cursor/selection, or down the priority list. This way we guard against the refactorings from overpolluting the light bulb and also push down the less relevant/close proximity refactorings down into a nested menu for discoverability. Basically, we have 2 separate sources for light bulb actions - code fixes and refactorings. Both of them choose different model where former uses a concrete span for communication between the engine and the extension, and have a very intuitive location where it will be offered. The later chose a model where the input span is a hint for extension to do further processing to determine applicability, and when multiple refactorings register actions, the engine has no way to determine the most applicable one. We haven't had any problems for discoverability of code fixes, but have had issues with refactorings as mentioned by @jmarolf and also found by @kendrahavens when discussing with customers. Yes, we can take the approach to find tune refactorings that customer complain about, but this does not prevent future refactorings from going down the same path and hitting same issues. I think fixing the engine/API would help us solve the issue for long term. |
I think it will. We commonly and continually refine and do better. Note that i don't think teh refactoring engine has enough information to decide what to do. For example, it's very important that some refactorings be made available on certain parts of a construct, but not the whole construct. We'd need an api to signify that and we'd still need refactorings to use that properly. In many cases, it's really case specific and the refactoring has to apply the right smarts no matter what.
Yes we have. Definitely fixes that are currently in the "no UI mode". It was a large enough deal that i had to go through and fixup many of them to address issues here (both showing up too much, and not showing up enough). That's precisely what drove me to open several PRs on a framework for how a feature should work with all severity levels, because we were having so many problems here. That framework was rejected at the time, even though this was an area i was trying to improve :) |
I think it's worse than that. I think many refactorings simply don't register when appropriate. Nothing about the engine changing will affect that (unless you literally ask to refactor every char on a line and you aggregate the results somehow). This will invariably require changing the refactorings for whatever new system you have. So i proprose just skipping that middle step and fixing up the refactorings that are problematic. Note: we've done this numerous times very successfully. There are complains about new refactorings, and that doesn't surprise me since we didn't focus on this during the reviews (since they were enormous) and we didn't invest any time on them fixing them up after the fact. That's different from almost every other refactoring we've written. We normally would spend a fair amount of tiem thinking about "where are the places this should (and, importantly) not show up?", and we would rapidly tweak the refactoring soon after release. Now, we've released the refactoring, spent little (if any) time on doing any fixups here, and are extrapolating out now that we have a significant problem that needs addressing. I don't buy it. I want to see the outcome of just taking the simple and cheap approach that we've always had, before deciding it's time to throw the baby out with the bathwater. |
Let's look at that a second. Consider "generate equals and hashcode". One thing we heard from people was that they expected to be able to go to a blank line in a class and say "i want to generate these guys here". How does it work to convert this to a diagnostic+span? Would there be a span for every blank line? |
I'm not sure what this means. tokens/nodes are a c#/vb-ism. How does this work for TypeScript/JavaScript/F#? Determining the token from a positoin is also well defined for C#/vb. but it's unclear waht it means to 'determine a node'. That is often very refactoring specific, with lots of necessarily logic to figure out what is sensible on a per-construct basis. |
Of all the approaches, impl3 make the most sense to me. But i don't see it being much different from what i'm proposing. Namely, that the refactoring is in charge. :) It lists this as a CON:
But htat's not a con for me. Refactorings must think about this. For example, we know from direct experience that code-fixes that specify too broad a range of applicability are really unpleasant. The same is true for refactorings. We often do not want them diving into certain children (like lambdas) because of how noisy and unintuitive the experience seems. The driving scenarios here (for/foreach/linq) def suffer from this, and we'd want to customize the approach for these features. So i would def drive this by trying to actually fix these actual user issues and seeing what we can grok out of it. Right now i think we're examining potential options without actually knowing if they would fit the very scenarios we're trying to fix. |
This diagnostic span would potentially be the entire type declaration. The refactoring will only show up in top level menu if you are directly touching node or token in the type declaration, and will show up in a nested menu if you need to walk a step up in parent chain.
The goal here is to improve the experience for C#/VB and all languages that have documents based on syntax (SupportsSyntaxTree). We would leave the current approach of delegating to refactoring to make these choices for other languages.
That seems unrelated. Diagnostics giving too broad spans have visual effect of huge squiggles that is unpleasant. Hidden diagnostics are never affected by this issue. |
Note: i'm very much not opposed to the idea of a larger-scale fix here (in case that's how my earlier posts came across). What i'm trying to emphasize though is that we should relaly be certain that is necessary, as opposed to it being the case that we have a couple of refactorings that we simply shipped quickly, and which we could improve with 5 mins of effort :) |
Note: i reallllly don't like that idea. If we have an idea for how to make this better, it should be better for all langs :) Note that i think this is definitely possible. |
But is that the experience we want? i.e. if someone liked being able to generate these members by going to a specific gap between members, now they need to go into this sub-menu (which i'm guessing will rarely be looked at). Furthermore, it would be really unpleasant if we had this submenu and it basically showed everything that had a span that crossed it. Basically any 'type' or 'member' refactoring would always show up in this submenu inside a member, no matter how off it seemed. That seems like exactly the type of noisiness we were working toward moving away from :) |
Again, please don't take this as me being ultra negative. I'm strongly in favor of a good user experience here, and driving that through tech. But i want to think about waht we want the user experience to be and then solve the tech problem. Not: create a tech solution, but potentially have it cause a poor experience. Thanks! |
Thanks @CyrusNajmabadi. The primary reason we started discussing discoverability of code actions was an outcome of customer surveys for many customers across multiple refactorings, plus internal feedback from customers indicating that they either did not find out we had xyz refactoring or even if they did, they had trouble identifying how to invoke them. @kendrahavens @vatsalyaagrawal and @jinujoseph can give more background here on the reasons why discoverability of refactorings has come up as a top issue recently. If the issue is just related to 1-2 refactorings, I would be more then happy to avoid additional work from new API, engine changes or creating a new UI/tool window for code actions :) |
They definitely are. For example, we used hidden diagnostics for things like "convert a block back to an expression" (or vice versa) depending on what your actual option was. it took a lot of fine-tuning of that to not make it feel noisy. Here's why: Even if there's no squiggle, if you're on the invisible-diagnostic-span:
This is not speculative mind you. This is directly from having feedback and our direct experiences using this. Again, this is why i was trying to come up with solutions on this earlier. Indeed, one solution was to stop using diagnostic analyzers here because they gave such a problematic experience for the invisible-level dianostics. :) |
Design review conclusion:
We are planning this as one of our team's summer intern projects. In addition to the above, I suggested it would be nice to have the ability to start typing with the light bulb menu visible to filter the list using the algorithms we already have for completion. With a few predictable characters, users will be able to reliably trigger refactorings even if items get added and/or rearranged in the future. |
FYI, we may want to increase the range that "Convert anonymous type to class" is offered: https://developercommunity.visualstudio.com/content/idea/651687/creating-a-class-from-annonomous-type.html |
@kendrahavens from reading that, it sounds more like they don't really know how to invoke lightbulbs, or that they're looking for a specific keystroke to perform this specific refactorings. |
Common helpers:Current situation:The initial work for implementation 4 helpers has been mostly done, a common set of helpers in In addition to decreased complexity (potentially easier maintanance) within the individual Refactorings it also brought consistency for when individual Refactorings are offered, especially but not limited to when a user uses seletion.
The common set of rules for when a Refactoring should be offered:The general idea behind the helpers is based on the assumption that Refactorings work on nodes. For example ConvertLINQToFor works on LINQ query node, ConvertForToForeach on For node. With that assumption the helper takes current selection/caret location and tries to find out if a desired node is selected (in case of selection) / logically near (in case of caret location). If it is, it returns such node. Being near means for example being on the edge of a the node or selecting the whole node. Specific rules are explained below.
Extractions:Since the SyntaxTree sometimes doesn't map to how a user might think about the code a set of extractions is done for each node that is considered by rules below. For example a user might select a whole declaration In essence it makes
Non-empty selections:
Empty selections (just caret location):
Present:
Future:
|
Ordering of available Refactorings/CodeActions:When a user invokes CodeActions menu (ctrl+.) individual elements (Refactorings & CodeFixes) are gathered and presented to user. These CodeActions are sorted in some way. Following paragraphs describes how. Current situation:
The big issue is that It also means that Refactorings' distance of
TL;DR:Generally, current ordering of CodeFixes can be summarized as follows:
|
Ordering of available Refactorings/CodeActions:Proposed solution:The proposals above revolve around giving the service more information so that it is able to order Refactorings CodeActions better. Using Since most refactorings operate on nodes & we have a standardized way of getting the nodes (see Common helpers above) there's also a convenient source of Therefore the proposal is to:
Issues:Implementation issues:Some Refactorings might not necessarily operate on nodes. For example generateXXX Refactorings can generate new nodes anywhere in root of a type. For these refactorings, however, we can simply specify their That will cause the distance to current carret / selection being larger then for all other available refactorings which is the desired state. Generally, in conjunction with using Priority this shouldn't be pose a real problem. Migration issues:
Limitations of using
|
Relevant: #33818 |
A bunch of recent customer surveys done by @kendrahavens identified that quite a few customers find our refactorings to be not discoverable. The refactorings that they requested were already implemented by us, but they needed Kendra to point out where to put the cursor or how to change the selection span for these to show up in the light bulb menu. The same concern does not apply to code fixes due to visual cue from the squiggle/suggestion dots in the UI, and pressing Ctrl + dot anywhere on the line shows up the fix, ordering being based on the distance from the diagnostic span. Note that the primary reason why we don't show up all the refactorings available on a given line are to avoid overloading the light bulb menu, which is already quite noisy. We need to fine tune the experience here to find a balance between discoverability of actions and overloading the light bulb menu.
We have talked about adding 2 different discoverability enhancements to address these concerns:
Show additional actions in light bulb menu
Improve the discoverability of the refactorings in the light bulb, by showing additional, but likely less relevant actions, that are applicable for positions near the current cursor or selection span. Most relevant refactorings would still be show at the top of the menu and these additional actions will either be shown at the bottom of the menu or nested within a separate menu towards the bottom. Internal discussions have led to bunch of different implementation suggestions on how to achieve this (discussed below), but the primary conclusion being that we need to somehow associate a fix span/ideal span with each registered code action within a refactoring, so the engine can prioritize the ordering and/or nesting of these refactorings based on promixity of this span with current cursor position or selection span.
Possible implementation approaches that came out:
Implementation 1: Convert all the IDE refactorings that are not selection based into a pair of diagnostic analyzer reporting hidden diagnostics + code fix. The diagnostic span would be the ideal span for the code actions.
PROS:
CONS:
Implementation 2: Currently, the refactoring service executes naively for identifying available actions for current cursor position or span. It passes in the entire line span or selection span into each refactoring, and then treats all registered refactorings to be on par with each other. This forces our refactorings to then be implemented in a restrictive manner, so they are not offered everywhere on the line and do not overload the light bulb menu. This whole setup relies on the assumption that users are already aware about where to put their cursor or what span to select to get the relevant refactorings, which does not seem to be true as mentioned at the start of this post. This proposal tries to remove this assumption by making the following changes:
PROS:
1. We do not alter the existing light bulb menu significantly for advanced/experienced users, while adding a new discoverability point for beginner users to discover new potential actions in nearby locations.
2. The implementation for each refactoring is greatly simplified and unified as they only work when input span exactly matches it's fixed span.
CONS:
1. We might end up with a perf hit due to the code refactoring service doing multiple passes. We would need perf measurements to identify if this indeed a concern as most refactorings would just bail out upfront.
2. We need to experiment/decide if a nested menu is indeed a good discoverability point as beginner users might not know that they need to dive into a nested menu at the bottom.
PROS: Get the similar user experience as prior approaches, with potentially lesser implementation cost then approach i. and avoid multiple passes that are needed in approach ii.
CONS: Adds implementation complexity in each refactoring of identifying multiple nodes/tokens of interest and then register each action with a span.
New UI for viewing available actions in a broader scope: Create a separate tool window to show available code actions within a given scope (document/project/solution, with document being the default). Few open questions:
We would potentially start with a simple UI, that only works for document scope to start with, and iterate on improving it to work with broader scopes.
The text was updated successfully, but these errors were encountered: