-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Integrate Compose Helper Plugin #1768
base: master
Are you sure you want to change the base?
Integrate Compose Helper Plugin #1768
Conversation
added comments wherever necessary formatted with ktlint
Thanks for your contribution, we're working on reviewing it! |
@@ -0,0 +1,70 @@ | |||
package org.jetbrains.compose.intentions.wrap_with_composable.wrap_with_actions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better use camelCaseNaming.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
camelCase for the package names?
and renamed package names
@olonho I've done the necessary changes. Please have a look |
<intentionAction id="wrap_group" order="first"> | ||
<className>org.jetbrains.compose.intentions.WrapWithComposableIntentionGroup | ||
</className> | ||
<category>Composable intentions</category> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Usually, intentions category don't have the word "intentions" (you can check that by looking at the "Preferences | Editor | Intentions" in Intellij).
Android Compose uses Android Compose
category for that, so I suggest changing the value to Compose Multiplatform
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Understood! I'll change that. Thanks!
|
||
<defaultLiveTemplates file="templates/ComposeMultiplatformTemplates.xml"/> | ||
|
||
<intentionAction id="wrap_group" order="first"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do id
actually necessary at any of those?
Generally I'm OK with having them, but not unqualified (i.e. I suggest adding a package prefix, so ids don't accidentally clash with another plugin).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's really not necessary. I was trying out if we could change the order of intentions using the "order" attribute. I'll remove both of them
<?xml version="1.0" encoding="UTF-8"?> | ||
<templateSet group="ComposeMultiplatformTemplates"> | ||
|
||
<template name="WwB" description="Wrap with Box" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like there is a discrepancy between the name of the template and what it does, when used as a template.
Wrap-* actions as intentions work as expected.
However, templates can be typed in the editor by the name, e.g. here Wrap with Box
can be typed as WwB
.
But the result of choosing the template in the completion is not, what I would expect from the word wrap
.
Consider the following example:
Column {
Row {
// the editor caret is here
}
}
If I type WwB
at the caret location and press enter, I get the following code:
Column {
Row {
Box(modifier = Modifier) {
}
}
}
In my opinion, that's not wrapping, this is an insertion of Box.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. I had not thought about Live templates from that angle. My Intention does the wrapping. Live template inserts the respective Composable, like you said. Shall I just rename them?
<templateSet group="ComposeMultiplatformTemplates"> | ||
|
||
<template name="WwB" description="Wrap with Box" | ||
value="$COMPOSABLE$(modifier = androidx.compose.ui.Modifier) { $SELECTION$ }" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if adding modifier argument by default is better, than not adding it. Probably add/remove modifier could be another intention, but I don't have a strong opinion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Understood! I asked around a few people regarding this. Even though some of them said they needed the default modifier, most of them did not. So I'll remove it. :)
<html lang="en"> | ||
<body> | ||
<p> | ||
A simple intention to remove the composable altogether. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should it be "a Composable" in this context? I also think it should be capitalized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes! That sounds better. Will do that
</intentionAction> | ||
|
||
<intentionAction id="remove_parent_composable" order="last"> | ||
<className>org.jetbrains.compose.intentions.RemoveParentComposableIntention |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this intention works as intended.
In the following example I see a few issues:
@Composable
fun ExampleComposable() {
Column {
Row {
Box {
}
}
}
}
- The intention is available on
Column
, but unavailable onBox
. I don't understand, what parent Composable can be removed onColumn
in this example. - Using intention on
Row
orColumn
(in the example) does the same thing asremove Composable
.
Maybe I misunderstood the meaning behind the intention?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh. I'm sorry, but I think you misunderstood the feature.
Remove Composable removes everything - Selected and its children.
Remove Parent Composable just removes the Selected, but not its children.
In the above example, it'll only be available in case of Column, Row, and not on Box (since it doesnt have any children)
import org.jetbrains.compose.intentions.utils.composableFinder.ComposableFunctionFinder | ||
import org.jetbrains.kotlin.idea.KotlinLanguage | ||
|
||
interface IsIntentionAvailable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see any reason to introduce a package and an interface just to namespace a single function.
Why not just put all these little functions inside one or few files?
In other words, how current implementation is better than the following?
package org.jetbrains.compose.intentions.utils
fun PsiElement.isComposableIntentionAvailable(
composableFunctionFinder: ComposableFunctionFinder
): Boolean {
if (language == KotlinLanguage.INSTANCE && isWritable) {
val parent = parent ?: return false
return composableFunctionFinder.isFunctionComposable(parent)
}
return false
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Understood! That can be done, yes!
I just personally dont like extension functions being available throughout the project.
Since it's a single function, I'll do as you say
* 4. KtValueArgumentList - () | ||
*/ | ||
tailrec operator fun invoke(element: PsiElement, iteration: Int = 0): PsiElement? { | ||
// To avoid infinite loops |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think an infinite loop is possible, since you only move up on a syntax tree.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sorry, there wont be an 'infinite' loop per se. I just added it to cover all edge cases. I shall remove it
return "Wrap with Composable" | ||
} | ||
|
||
override fun getIcon(flags: Int): Icon = PreviewIcons.COMPOSE |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest to avoid overriding an icon for any intention
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yess! I shall remove it. That was one of the questions I had asked in the original post.
|
||
override fun isFunctionComposable(psiElement: PsiElement): Boolean { | ||
return when (psiElement) { | ||
is KtNameReferenceExpression -> psiElement.isComposable() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about limiting the scope of intentions to just KtNameReferenceExpression
?
I.e. dropping KtCallExpression
, KtProperty
and KtValueArgumentList
.
I'm just not sure that the current implementation is intuitive and coherent.
For example:
Composable1(modifier = Modifier) {}
In the snippet above, the intentions are available at Composable1
(which makes intuitive sense), at (
and )
, but not at modifier = Modifier
and not at {}
.
There are lots of types of elements in Kotlin PSI, I'm not sure that the current implementation will behave consistently. At the same time it complicates the behavior of the intention as well. For example, I can't clearly articulate, what GetRootPsiElement
does, and why it is written in the way it is without taking into account this function. Narrowing the scope of intentions can make it easier to both use intentions and reason about code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AlexeyTsvetkov
We can remove the KtCallExpression check. As per your example, it currently works on 'Composable' and '('. We would need 'KtValueArgumentList' for detecting on () like you said. {} is not included, of course.
KtProperty can be skipped, but you'll lose the ability to use it with properties. Currently, you can use it on 'val/var', 'property name', and the composable function like 'remember/animateAsFloat'. If you wish to take that call, I can remove it.
So, what GetRootPsiElement does is, if you invoke it on a PsiElement, it gives you the parent PsiElement.
For a composable function, it can be the KtCallExpression.
For a delegatedProperty, it will be KtProperty.
The root psi element is them wrapped with / deleted respectively. You can check out the GetRootPsiElementTest for more info
renamed live template names cleaned up interfaces
@AlexeyTsvetkov I've made the necessary changes and answered some of your questions above. Please have a look at them when you have time. Thank you! |
Changes
Notes -
Screenshot