[kotlin] KTIJ-35016 Introduce observable UI settings for move refactoring

Settings allow sharing UI state between dependent move model parts.
This is necessary for conditional rendering of widgets.
For example, simultaneous move of expects with actuals needs a slightly customized UI.

GitOrigin-RevId: 722cc642ec192990113b866fa38a70c97f8ac323
This commit is contained in:
Pavel Kirpichenkov
2025-11-05 21:17:30 +00:00
committed by intellij-monorepo-bot
parent c8a4b34bd2
commit c9d485e340
3 changed files with 96 additions and 16 deletions
@@ -2,6 +2,9 @@
package org.jetbrains.kotlin.idea.k2.refactoring.move.ui
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.observable.properties.AtomicBooleanProperty
import com.intellij.openapi.observable.properties.MutableBooleanProperty
import com.intellij.openapi.observable.properties.ObservableBooleanProperty
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.openapi.util.NlsContexts
@@ -38,7 +41,11 @@ import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
/**
* @see K2MoveDescriptor
*/
sealed class K2MoveModel {
sealed class K2MoveModel(observableUiSettings: ObservableUiSettings) : K2MoveModelObservableSettings {
init {
observableUiSettings.registerK2MoveModelSettings(this)
}
abstract val project: Project
abstract val source: K2MoveSourceModel<*>
@@ -81,6 +88,18 @@ sealed class K2MoveModel {
}
}
override val mppDeclarationsObservable: ObservableBooleanProperty
get() = mppDeclarations.observableProperty
override val searchForTextObservable: ObservableBooleanProperty
get() = searchForText.observableProperty
override val searchInCommentsObservable: ObservableBooleanProperty
get() = searchInComments.observableProperty
override val searchReferencesObservable: ObservableBooleanProperty
get() = searchReferences.observableProperty
enum class Setting(private val text: @NlsContexts.Checkbox String) {
SEARCH_FOR_TEXT(KotlinBundle.message("search.for.text.occurrences")) {
override var state: Boolean
@@ -124,12 +143,16 @@ sealed class K2MoveModel {
};
abstract var state: Boolean
// lazy prevents service access from constructor
internal val observableProperty: MutableBooleanProperty by lazy { AtomicBooleanProperty(state) }
fun createComboBox(panel: Panel, enabled: Boolean = true) {
panel.row {
val checkBox = checkBox(text).enabled(enabled)
if (enabled) {
checkBox.bindSelected(::state)
// bind doesn't register state changes before dialog confirmation, but the UI should be updated on all state changes
checkBox.onChanged { observableProperty.set(it.isSelected) }
} else {
checkBox.selected(false)
}
@@ -145,8 +168,9 @@ sealed class K2MoveModel {
override val source: K2MoveSourceModel.FileSource,
override val target: K2MoveTargetModel.SourceDirectory,
override val inSourceRoot: Boolean,
override val moveCallBack: MoveCallback? = null
) : K2MoveModel() {
observableUiSettings: ObservableUiSettings,
override val moveCallBack: MoveCallback? = null,
) : K2MoveModel(observableUiSettings) {
private fun PsiFile.isAlreadyInTarget(): Boolean {
return parent == target.directory && when (this) {
is PsiJavaFile -> packageName == target.pkgName.asString()
@@ -192,8 +216,9 @@ sealed class K2MoveModel {
override val source: K2MoveSourceModel.ElementSource,
override val target: K2MoveTargetModel,
override val inSourceRoot: Boolean,
observableUiSettings: ObservableUiSettings,
override val moveCallBack: MoveCallback? = null
) : K2MoveModel() {
) : K2MoveModel(observableUiSettings) {
private fun isValidFileRefactoring(fileName: String): Boolean {
fun KtFile.isTargetFile(): Boolean {
return containingDirectory == target.directory
@@ -307,6 +332,7 @@ sealed class K2MoveModel {
}
val inSourceRoot = isInSourceRoot(project, elementsToMove, targetContainer)
val explicitPkgMoveFqName = findExplicitPkgMoveFqName(elementsToMove)
val observableUiSettings = ObservableUiSettingsImpl()
return when {
(elementsToMove.all { it is KtFile } && targetContainer is PsiDirectory)
@@ -321,22 +347,23 @@ sealed class K2MoveModel {
pkgName = pkg,
directory = targetContainer,
explicitPkgMoveFqName = explicitPkgMoveFqName,
observableUiSettings = observableUiSettings,
)
} else { // no default target is provided, happens when invoking refactoring via keyboard instead of drag-and-drop
val file = elementsToMove.firstOrNull { it.containingFile != null }?.containingFile
?: error("No default target found")
val directory = file.containingDirectory ?: error("No default target found")
val pkgName = elementsToMove.firstIsInstanceOrNull<KtElement>()?.containingKtFile?.packageFqName ?: FqName.ROOT
K2MoveTargetModel.SourceDirectory(pkgName, directory, explicitPkgMoveFqName = null)
K2MoveTargetModel.SourceDirectory(pkgName, directory, explicitPkgMoveFqName = null, observableUiSettings)
}
Files(project, source, target, inSourceRoot, moveCallBack)
Files(project, source, target, inSourceRoot, observableUiSettings, moveCallBack)
}
targetContainer is KtFile || targetContainer.isSingleClassContainer() || isSingleFileMove(elementsToMove) -> {
val source = K2MoveSourceModel.ElementSource(declarationsFromFiles.toSet())
val targetFile = targetContainer?.containingFile
val target = if (targetFile is KtFile) {
K2MoveTargetModel.File(targetFile)
K2MoveTargetModel.File(targetFile, observableUiSettings)
} else if (targetContainer is PsiDirectory) {
val pkg = targetContainer.getFqNameWithImplicitPrefixOrRoot()
K2MoveTargetModel.File(
@@ -344,6 +371,7 @@ sealed class K2MoveModel {
pkg = pkg,
directory = targetContainer,
explicitPkgMoveFqName = explicitPkgMoveFqName,
observableUiSettings = observableUiSettings,
)
} else { // no default target is provided, happens when invoking refactoring via keyboard instead of drag-and-drop
val firstElem = elementsToMove.firstOrNull() as KtElement
@@ -352,7 +380,8 @@ sealed class K2MoveModel {
K2MoveTargetModel.Declarations(
defaultDirectory = psiDirectory,
defaultPkgName = containingFile.packageFqName,
defaultFileName = findSourceFileNameByMovedElements(elementsToMove)
defaultFileName = findSourceFileNameByMovedElements(elementsToMove),
observableUiSettings = observableUiSettings,
)
}
@@ -361,6 +390,7 @@ sealed class K2MoveModel {
source = source,
target = target,
inSourceRoot = inSourceRoot,
observableUiSettings = observableUiSettings,
moveCallBack = moveCallBack
)
}
@@ -46,6 +46,8 @@ sealed interface K2MoveTargetModel {
val explicitPkgMoveFqName: FqName?
val observableUiSettings: ObservableUiSettings
/**
* Creates a [K2MoveTargetDescriptor] from this model.
* @param kmpSourceRoot source KMP directory that corresponds to the base [directory] in a different source set.
@@ -60,6 +62,7 @@ sealed interface K2MoveTargetModel {
override var pkgName: FqName,
override var directory: PsiDirectory,
override val explicitPkgMoveFqName: FqName?,
override val observableUiSettings: ObservableUiSettings,
) : K2MoveTargetModel {
private val initialDirectory = directory
@@ -147,7 +150,8 @@ sealed interface K2MoveTargetModel {
pkgName: FqName,
directory: PsiDirectory,
explicitPkgMoveFqName: FqName?,
) : SourceDirectoryChooser(pkgName, directory, explicitPkgMoveFqName) {
observableUiSettings: ObservableUiSettings,
) : SourceDirectoryChooser(pkgName, directory, explicitPkgMoveFqName, observableUiSettings) {
override fun toDescriptor(kmpSourceRoot: PsiDirectory?): K2MoveTargetDescriptor.Directory {
return K2MoveTargetDescriptor.Directory(
pkgName = pkgName,
@@ -167,7 +171,8 @@ sealed interface K2MoveTargetModel {
pkg: FqName,
directory: PsiDirectory,
explicitPkgMoveFqName: FqName?,
) : SourceDirectoryChooser(pkg, directory, explicitPkgMoveFqName) {
observableUiSettings: ObservableUiSettings,
) : SourceDirectoryChooser(pkg, directory, explicitPkgMoveFqName, observableUiSettings) {
var fileName: String = fileName
protected set
@@ -221,8 +226,13 @@ sealed interface K2MoveTargetModel {
}
}
class File(fileName: String, pkg: FqName, directory: PsiDirectory, explicitPkgMoveFqName: FqName?) :
FileChooser(fileName, pkg, directory, explicitPkgMoveFqName) {
class File(
fileName: String,
pkg: FqName,
directory: PsiDirectory,
explicitPkgMoveFqName: FqName?,
observableUiSettings: ObservableUiSettings,
) : FileChooser(fileName, pkg, directory, explicitPkgMoveFqName, observableUiSettings) {
override fun toDescriptor(kmpSourceRoot: PsiDirectory?): K2MoveTargetDescriptor.File =
K2MoveTargetDescriptor.File(
fileName = fileName,
@@ -242,8 +252,9 @@ sealed interface K2MoveTargetModel {
class Declarations(
defaultDirectory: PsiDirectory,
defaultPkgName: FqName,
defaultFileName: String
) : FileChooser(defaultFileName, defaultPkgName, defaultDirectory, explicitPkgMoveFqName = null) {
defaultFileName: String,
observableUiSettings: ObservableUiSettings,
) : FileChooser(defaultFileName, defaultPkgName, defaultDirectory, explicitPkgMoveFqName = null, observableUiSettings) {
private val propertyGraph = PropertyGraph()
private val destinationClassProperty = propertyGraph.property<KtClassOrObject?>(null)
@@ -354,9 +365,15 @@ sealed interface K2MoveTargetModel {
}
companion object {
fun File(file: KtFile): File {
fun File(file: KtFile, observableUiSettings: ObservableUiSettings): File {
val directory = file.containingDirectory ?: error("No containing directory was found")
return File(file.name, file.packageFqName, directory, null)
return File(
fileName = file.name,
pkg = file.packageFqName,
directory = directory,
explicitPkgMoveFqName = null,
observableUiSettings = observableUiSettings,
)
}
}
}
@@ -0,0 +1,33 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.k2.refactoring.move.ui
import com.intellij.openapi.observable.properties.ObservableBooleanProperty
interface ObservableUiSettings : K2MoveModelObservableSettings {
fun registerK2MoveModelSettings(settings: K2MoveModelObservableSettings)
}
interface K2MoveModelObservableSettings {
val searchReferencesObservable: ObservableBooleanProperty
val searchForTextObservable: ObservableBooleanProperty
val searchInCommentsObservable: ObservableBooleanProperty
val mppDeclarationsObservable: ObservableBooleanProperty
}
internal class ObservableUiSettingsImpl : ObservableUiSettings {
private lateinit var k2MoveModelSettings: K2MoveModelObservableSettings
override val searchReferencesObservable: ObservableBooleanProperty
get() = k2MoveModelSettings.searchReferencesObservable
override val searchForTextObservable: ObservableBooleanProperty
get() = k2MoveModelSettings.searchForTextObservable
override val searchInCommentsObservable: ObservableBooleanProperty
get() = k2MoveModelSettings.searchInCommentsObservable
override val mppDeclarationsObservable: ObservableBooleanProperty
get() = k2MoveModelSettings.mppDeclarationsObservable
override fun registerK2MoveModelSettings(settings: K2MoveModelObservableSettings) {
k2MoveModelSettings = settings
}
}