diff --git a/.idea/modules.xml b/.idea/modules.xml index 17be73e1e72c..fca59ffc5706 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -919,6 +919,7 @@ + diff --git a/intellij.idea.community.main.iml b/intellij.idea.community.main.iml index 25722bc74676..b7f8a8fee76a 100644 --- a/intellij.idea.community.main.iml +++ b/intellij.idea.community.main.iml @@ -150,6 +150,7 @@ + diff --git a/plugins/settings-sync/fileSystem/intellij.settingsSync.fileSystem.iml b/plugins/settings-sync/fileSystem/intellij.settingsSync.fileSystem.iml new file mode 100644 index 000000000000..cf071115fce2 --- /dev/null +++ b/plugins/settings-sync/fileSystem/intellij.settingsSync.fileSystem.iml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/settings-sync/fileSystem/resources/intellij.settingsSync.fileSystem.xml b/plugins/settings-sync/fileSystem/resources/intellij.settingsSync.fileSystem.xml new file mode 100644 index 000000000000..2fb6ece84276 --- /dev/null +++ b/plugins/settings-sync/fileSystem/resources/intellij.settingsSync.fileSystem.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/plugins/settings-sync/fileSystem/resources/messages/BackupNSyncFSBundle.properties b/plugins/settings-sync/fileSystem/resources/messages/BackupNSyncFSBundle.properties new file mode 100644 index 000000000000..c650fde0bbca --- /dev/null +++ b/plugins/settings-sync/fileSystem/resources/messages/BackupNSyncFSBundle.properties @@ -0,0 +1 @@ +action.settingsSync.enableRemotely.text = Enable Backup Sync (from FS) \ No newline at end of file diff --git a/plugins/settings-sync/fileSystem/src/intellij/settingsSync/fileSystem/EnableBackupNSyncRemotely.kt b/plugins/settings-sync/fileSystem/src/intellij/settingsSync/fileSystem/EnableBackupNSyncRemotely.kt new file mode 100644 index 000000000000..01e3509bb27e --- /dev/null +++ b/plugins/settings-sync/fileSystem/src/intellij/settingsSync/fileSystem/EnableBackupNSyncRemotely.kt @@ -0,0 +1,52 @@ +package intellij.settingsSync.fileSystem + +import com.intellij.ide.util.PropertiesComponent +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.fileChooser.FileChooserFactory +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.settingsSync.SettingsSyncLocalSettings +import com.intellij.settingsSync.SettingsSyncSettings +import com.intellij.settingsSync.UpdateResult.* +import com.intellij.settingsSync.config.SettingsSyncEnabler +import com.intellij.util.containers.toMutableSmartList + +class EnableBackupNSyncRemotely : DumbAwareAction() { + override fun getActionUpdateThread() = ActionUpdateThread.BGT + + override fun actionPerformed(e: AnActionEvent) { + ApplicationManager.getApplication().invokeLater { + val folderDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor() + .withTitle("Select root folder") + val chooser = FileChooserFactory.getInstance().createPathChooser(folderDescriptor, null, null) + chooser.choose(null) { + val path = it.single().path + val availableAccounts = (PropertiesComponent.getInstance().getList("FSAuthServiceAccounts") ?: emptyList()).toMutableSmartList() + availableAccounts.add(path) + SettingsSyncLocalSettings.getInstance().userId = path + SettingsSyncLocalSettings.getInstance().providerCode = "fs" + PropertiesComponent.getInstance().setList("FSAuthServiceAccounts", availableAccounts) + } + SettingsSyncSettings.getInstance().syncEnabled = true + val enabler = SettingsSyncEnabler() + val serverState = enabler.getServerState() + when (serverState) { + is NoFileOnServer, FileDeletedFromServer -> { + enabler.pushSettingsToServer() + } + is Success -> { + enabler.getSettingsFromServer(null) + } + is Error -> { + logger().error(serverState.message) + } + } + } + } + + + +} \ No newline at end of file diff --git a/plugins/settings-sync/fileSystem/src/intellij/settingsSync/fileSystem/FSAuthService.kt b/plugins/settings-sync/fileSystem/src/intellij/settingsSync/fileSystem/FSAuthService.kt new file mode 100644 index 000000000000..ceb087d54bdb --- /dev/null +++ b/plugins/settings-sync/fileSystem/src/intellij/settingsSync/fileSystem/FSAuthService.kt @@ -0,0 +1,80 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package intellij.settingsSync.fileSystem + +import com.intellij.ide.util.PropertiesComponent +import com.intellij.openapi.application.EDT +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.fileChooser.FileChooserFactory +import com.intellij.platform.ide.progress.ModalTaskOwner +import com.intellij.platform.ide.progress.TaskCancellation +import com.intellij.platform.ide.progress.withModalProgress +import com.intellij.settingsSync.auth.SettingsSyncAuthService +import com.intellij.settingsSync.communicator.SettingsSyncUserData +import com.intellij.util.containers.mapSmart +import com.intellij.util.containers.toMutableSmartList +import kotlinx.coroutines.* +import java.awt.Component +import java.nio.file.Path +import javax.swing.Icon +import kotlin.io.path.absolutePathString +import kotlin.io.path.name +import kotlin.coroutines.resume + + +internal class FSAuthService : SettingsSyncAuthService { + override val providerCode: String + get() = "fs" + override val providerName: String + get() = "File System" + + override val icon: Icon? + get() = com.intellij.icons.AllIcons.Actions.ModuleDirectory + + override suspend fun login(parentComponent: Component?): SettingsSyncUserData? { + val modalTaskOwner = if (parentComponent != null) + ModalTaskOwner.component(parentComponent) + else + ModalTaskOwner.guess() + return withModalProgress(modalTaskOwner, "Getting data", TaskCancellation.cancellable(), ) { + val folderDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor() + .withTitle("Select root folder") + withContext(Dispatchers.EDT) { + val chooser = FileChooserFactory.getInstance().createPathChooser(folderDescriptor, null, null) + suspendCancellableCoroutine { cont -> + cont.invokeOnCancellation { + cont.resume(null) + } + chooser.choose(null) { + val path = it.single().path + val userData = userDataFromPath(path) + val availableAccounts = (PropertiesComponent.getInstance().getList("FSAuthServiceAccounts") ?: emptyList()).toMutableSmartList() + if (!availableAccounts.contains(path)) { + availableAccounts.add(path) + PropertiesComponent.getInstance().setList("FSAuthServiceAccounts", availableAccounts) + } + cont.resume(userData) + } + } + } + } + } + + override fun getUserData(userId: String): SettingsSyncUserData? { + return getAvailableUserAccounts().find { it.id == userId } + } + + override fun getAvailableUserAccounts(): List { + return PropertiesComponent.getInstance().getList("FSAuthServiceAccounts")?.mapSmart { userDataFromPath(it) } ?: emptyList() + } + + private fun userDataFromPath(pathStr: String) : SettingsSyncUserData { + val path = Path.of(pathStr) + return SettingsSyncUserData( + path.absolutePathString(), + providerCode, + path.name, + "noname@email.com" + ) + } + +} diff --git a/plugins/settings-sync/fileSystem/src/intellij/settingsSync/fileSystem/FSCommunicator.kt b/plugins/settings-sync/fileSystem/src/intellij/settingsSync/fileSystem/FSCommunicator.kt new file mode 100644 index 000000000000..c71c2c769401 --- /dev/null +++ b/plugins/settings-sync/fileSystem/src/intellij/settingsSync/fileSystem/FSCommunicator.kt @@ -0,0 +1,59 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package intellij.settingsSync.fileSystem + +import com.intellij.openapi.diagnostic.logger +import com.intellij.settingsSync.* +import java.io.InputStream +import java.nio.file.Path +import kotlin.io.path.getLastModifiedTime + +internal class FSCommunicator(override val userId: String) : AbstractServerCommunicator() { + + private val basePath: Path = Path.of(userId) + + companion object { + private val LOG = logger() + } + + override fun requestSuccessful() { + LOG.info("requestSuccessful") + } + + override fun handleRemoteError(e: Throwable): String { + LOG.warn("remote error occurred", e) + return e.message ?: "Remote error occurred: ${e.javaClass.name}" + } + + override fun readFileInternal(filePath: String): Pair { + val path = basePath.resolve(filePath) + val file = path.toFile() + if (file.exists()) { + return Pair(file.inputStream(), path.getLastModifiedTime().toString()) + } + return Pair(null, null) + } + + override fun writeFileInternal(filePath: String, versionId: String?, content: InputStream): String? { + val path = basePath.resolve(filePath) + if (path.toFile().exists() && path.getLastModifiedTime().toString() != versionId) { + throw InvalidVersionIdException("Expected versionId is $versionId, but actual is ${path.getLastModifiedTime()}") + } + if (!path.parent.toFile().exists()) { + path.parent.toFile().mkdirs() + } + path.toFile().outputStream().use { content.copyTo(it) } + return path.getLastModifiedTime().toString() + } + + override fun getLatestVersion(filePath: String): String? { + val path = basePath.resolve(filePath) + if (!path.toFile().exists()) + return null + return path.getLastModifiedTime().toString() + } + + override fun deleteFileInternal(filePath: String) { + val path = basePath.resolve(filePath) + path.toFile().delete() + } +} \ No newline at end of file diff --git a/plugins/settings-sync/fileSystem/src/intellij/settingsSync/fileSystem/FSCommunicatorProvider.kt b/plugins/settings-sync/fileSystem/src/intellij/settingsSync/fileSystem/FSCommunicatorProvider.kt new file mode 100644 index 000000000000..23f7acf16a77 --- /dev/null +++ b/plugins/settings-sync/fileSystem/src/intellij/settingsSync/fileSystem/FSCommunicatorProvider.kt @@ -0,0 +1,21 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package intellij.settingsSync.fileSystem + +import com.intellij.settingsSync.SettingsSyncRemoteCommunicator +import com.intellij.settingsSync.auth.SettingsSyncAuthService +import com.intellij.settingsSync.communicator.SettingsSyncCommunicatorProvider + +class FSCommunicatorProvider : SettingsSyncCommunicatorProvider { + private val authServiceLazy = lazy { FSAuthService() } + + override val providerCode: String + get() = "fs" + + override val authService: SettingsSyncAuthService + get() = authServiceLazy.value + + override fun createCommunicator(userId: String): SettingsSyncRemoteCommunicator? { + return FSCommunicator(userId) + } + +} \ No newline at end of file diff --git a/plugins/settings-sync/intellij.settingsSync.iml b/plugins/settings-sync/intellij.settingsSync.iml index 40ad4f9f5b5b..d458d58052a9 100644 --- a/plugins/settings-sync/intellij.settingsSync.iml +++ b/plugins/settings-sync/intellij.settingsSync.iml @@ -56,5 +56,6 @@ + \ No newline at end of file diff --git a/plugins/settings-sync/jba/intellij.settingsSync.jba.iml b/plugins/settings-sync/jba/intellij.settingsSync.jba.iml index 355c07bcaad0..9606e702516f 100644 --- a/plugins/settings-sync/jba/intellij.settingsSync.jba.iml +++ b/plugins/settings-sync/jba/intellij.settingsSync.jba.iml @@ -20,5 +20,6 @@ + \ No newline at end of file diff --git a/plugins/settings-sync/jba/resources/messages/SettingsSyncJbaBundle.properties b/plugins/settings-sync/jba/resources/messages/SettingsSyncJbaBundle.properties index 8501075a3072..33169b645543 100644 --- a/plugins/settings-sync/jba/resources/messages/SettingsSyncJbaBundle.properties +++ b/plugins/settings-sync/jba/resources/messages/SettingsSyncJbaBundle.properties @@ -1,4 +1,5 @@ action.settingsSync.troubleShoot.text=Backup and Sync Troubleshooting +login.manual.helper.text=In case of troubles please log in via Help->Register Plugins troubleshooting.loading.info.progress.dialog.title=Loading information about Backup and Sync\u2026 troubleshooting.dialog.title=Backup and Sync Troubleshooting diff --git a/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/CloudConfigServerCommunicator.kt b/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/CloudConfigServerCommunicator.kt index 2902fc4e8ae7..f2b85e265b79 100644 --- a/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/CloudConfigServerCommunicator.kt +++ b/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/CloudConfigServerCommunicator.kt @@ -56,8 +56,8 @@ internal open class CloudConfigServerCommunicator(serverUrl: String? = null, } @Throws(IOException::class) - override fun readFileInternal(snapshotFilePath: String): Pair { - return clientVersionContext.doWithVersion(snapshotFilePath, null) { filePath -> + override fun readFileInternal(filePath: String): Pair { + return clientVersionContext.doWithVersion(filePath, null) { filePath -> try { val stream = client.read(filePath) @@ -122,6 +122,9 @@ internal open class CloudConfigServerCommunicator(serverUrl: String? = null, client.delete(filePath) } + override val userId: String + get() = "jba" + override fun writeFileInternal(filePath: String, versionId: String?, content: InputStream) : String? { return clientVersionContext.doWithVersion(filePath, versionId) { filePath -> diff --git a/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/JbaCommunicatorProvider.kt b/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/JbaCommunicatorProvider.kt index 5c2fba3d68eb..369ae680364a 100644 --- a/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/JbaCommunicatorProvider.kt +++ b/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/JbaCommunicatorProvider.kt @@ -12,12 +12,13 @@ class JbaCommunicatorProvider : SettingsSyncCommunicatorProvider, Disposable { override val providerCode: String get() = "jba" + override val authService: SettingsSyncAuthService get() { return authServiceLazy.value } - override fun createCommunicator(): SettingsSyncRemoteCommunicator? = lazy { + override fun createCommunicator(userId: String): SettingsSyncRemoteCommunicator = lazy { CloudConfigServerCommunicator(null, authServiceLazy.value) }.value diff --git a/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/SettingsSyncJbaBundle.kt b/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/SettingsSyncJbaBundle.kt new file mode 100644 index 000000000000..9df8ef6e9923 --- /dev/null +++ b/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/SettingsSyncJbaBundle.kt @@ -0,0 +1,16 @@ +package com.intellij.settingsSync.jba + +import com.intellij.DynamicBundle +import org.jetbrains.annotations.ApiStatus +import org.jetbrains.annotations.Nls +import org.jetbrains.annotations.PropertyKey + +private const val PATH_TO_BUNDLE = "messages.SettingsSyncJbaBundle" + +@ApiStatus.Internal +object SettingsSyncJbaBundle { + private val bundle = DynamicBundle(SettingsSyncJbaBundle::class.java, PATH_TO_BUNDLE) + fun message(@PropertyKey(resourceBundle = PATH_TO_BUNDLE) key: String, vararg params: Any) : @Nls String { + return bundle.getMessage(key, *params) + } +} \ No newline at end of file diff --git a/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/SettingsSyncPromotion.kt b/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/SettingsSyncPromotion.kt index 561d46c09ee5..6149715a5eb0 100644 --- a/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/SettingsSyncPromotion.kt +++ b/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/SettingsSyncPromotion.kt @@ -79,7 +79,7 @@ class SettingsSyncPromotion : SettingsDialogListener { SettingsSyncEvents.Companion.getInstance().addListener(object : SettingsSyncEventListener { override fun loginStateChanged() { - if (RemoteCommunicatorHolder.getAuthService().isLoggedIn()) { + if (RemoteCommunicatorHolder.getCurrentUserData() != null) { SettingsSyncEventsStatistics.PROMOTION_IN_SETTINGS.log(SettingsSyncEventsStatistics.PromotionInSettingsEvent.LOGGED_IN) } } diff --git a/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/SettingsSyncTroubleshootingAction.kt b/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/SettingsSyncTroubleshootingAction.kt index 1e9b2563ec2d..99c98f718bd3 100644 --- a/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/SettingsSyncTroubleshootingAction.kt +++ b/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/SettingsSyncTroubleshootingAction.kt @@ -64,7 +64,12 @@ internal class SettingsSyncTroubleshootingAction : DumbAwareAction() { } override fun actionPerformed(e: AnActionEvent) { - val remoteCommunicator = RemoteCommunicatorHolder.getRemoteCommunicator() + val remoteCommunicator = RemoteCommunicatorHolder.getRemoteCommunicator() ?: run { + Messages.showErrorDialog(e.project, + "No remote communicator available", + SettingsSyncBundle.message("troubleshooting.dialog.title")) + return + } if (remoteCommunicator !is CloudConfigServerCommunicator) { Messages.showErrorDialog(e.project, SettingsSyncBundle.message("troubleshooting.dialog.error.wrong.configuration", remoteCommunicator::class), @@ -147,7 +152,7 @@ internal class SettingsSyncTroubleshootingAction : DumbAwareAction() { val remoteCommunicator: CloudConfigServerCommunicator, val rootNode: TreeNode.Branch) : DialogWrapper(project, true) { - val userData = RemoteCommunicatorHolder.getAuthService().getUserData() + val userData = RemoteCommunicatorHolder.getCurrentUserData() init { title = SettingsSyncBundle.message("troubleshooting.dialog.title") @@ -273,7 +278,7 @@ internal class SettingsSyncTroubleshootingAction : DumbAwareAction() { if (showHistoryButton) { actionButton(object : DumbAwareAction(AllIcons.Vcs.History) { override fun actionPerformed(e: AnActionEvent) { - showHistoryDialog(project, remoteCommunicator, version.filePath, userData.name!!) + showHistoryDialog(project, remoteCommunicator, version.filePath, userData?.name ?: "Unknown") } }) } diff --git a/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/auth/DummyJBAccountInfoService.kt b/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/auth/DummyJBAccountInfoService.kt index 39d798644572..3455ef22ebd5 100644 --- a/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/auth/DummyJBAccountInfoService.kt +++ b/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/auth/DummyJBAccountInfoService.kt @@ -22,7 +22,15 @@ object DummyJBAccountInfoService : JBAccountInfoService { } override fun startLoginSession(loginMode: JBAccountInfoService.LoginMode, authProviderId: String?, clientMetadata: Map): JBAccountInfoService.LoginSession { - TODO("Not yet implemented") + return object : JBAccountInfoService.LoginSession { + override fun close() { + + } + + override fun onCompleted(): CompletableFuture { + return CompletableFuture.completedFuture(JBAccountInfoService.LoginResult.LoginSuccessful(dummyUserData)) + } + } } override fun getAvailableLicenses(productCode: String): CompletableFuture { diff --git a/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/auth/JBAAuthService.kt b/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/auth/JBAAuthService.kt index 03bb5bd13330..e76889385f21 100644 --- a/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/auth/JBAAuthService.kt +++ b/plugins/settings-sync/jba/src/com/intellij/settingsSync/jba/auth/JBAAuthService.kt @@ -1,32 +1,38 @@ package com.intellij.settingsSync.jba.auth +import com.intellij.icons.AllIcons import com.intellij.openapi.application.ex.ApplicationManagerEx import com.intellij.openapi.diagnostic.logger +import com.intellij.platform.ide.progress.ModalTaskOwner +import com.intellij.platform.ide.progress.TaskCancellation +import com.intellij.platform.ide.progress.withModalProgress import com.intellij.settingsSync.SettingsSyncEvents import com.intellij.settingsSync.auth.SettingsSyncAuthService import com.intellij.settingsSync.communicator.SettingsSyncUserData +import com.intellij.settingsSync.jba.SettingsSyncJbaBundle import com.intellij.settingsSync.jba.SettingsSyncPromotion import com.intellij.ui.JBAccountInfoService -import java.util.function.Consumer +import kotlinx.coroutines.* +import java.awt.Component +import java.util.concurrent.CancellationException +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException internal class JBAAuthService : SettingsSyncAuthService { companion object { private val LOG = logger() + private const val JBA_USER_ID = "jba" } @Volatile private var invalidatedIdToken: String? = null - override fun isLoggedIn(): Boolean { - return isTokenValid(getAccountInfoService()?.idToken) - } - private fun isTokenValid(token: String?): Boolean { return token != null && token != invalidatedIdToken } - override fun getUserData() = fromJBAData( + override fun getUserData(userId: String) = fromJBAData( if (ApplicationManagerEx.isInIntegrationTest()) { DummyJBAccountInfoService.userData } else { @@ -34,12 +40,23 @@ internal class JBAAuthService : SettingsSyncAuthService { } ) - private fun fromJBAData(jbaData: JBAccountInfoService.JBAData?) : SettingsSyncUserData { + override fun getAvailableUserAccounts(): List { + val userData = getUserData("") + if (userData != null) { + return listOf(userData) + } else { + return emptyList() + } + } + + private fun fromJBAData(jbaData: JBAccountInfoService.JBAData?) : SettingsSyncUserData? { if (jbaData == null) { - return SettingsSyncUserData.EMPTY + return null } else { return SettingsSyncUserData( - jbaData.loginName, + JBA_USER_ID, + providerCode, + jbaData.email, jbaData.email, ) } @@ -53,9 +70,17 @@ internal class JBAAuthService : SettingsSyncAuthService { } override val providerCode: String get() = "jba" + override val providerName: String + get() = "JetBrains" - override fun login() { - if (!isLoggedIn()) { + override val icon = AllIcons.Ultimate.IdeaUltimatePromo + + override suspend fun login(parentComponent: Component?): SettingsSyncUserData? { + val modalTaskOwner = if (parentComponent != null) + ModalTaskOwner.component(parentComponent) + else + ModalTaskOwner.guess() + return withModalProgress(modalTaskOwner, SettingsSyncJbaBundle.message("login.manual.helper.text"), TaskCancellation.cancellable()) { val accountInfoService = getAccountInfoService() val loginMetadata = hashMapOf( "from.settings.sync" to "true" @@ -63,25 +88,55 @@ internal class JBAAuthService : SettingsSyncAuthService { if (SettingsSyncPromotion.promotionShownThisSession) { loginMetadata["from.settings.sync.promotion"] = "true" } - if (accountInfoService != null) { + if (accountInfoService == null) { + LOG.error("JBA auth service is not available!") + return@withModalProgress null + } + if (isTokenValid(accountInfoService.idToken)) { + return@withModalProgress fromJBAData(accountInfoService.userData) + } + suspendCancellableCoroutine { cont -> try { - val loginSession: JBAccountInfoService.LoginSession? = accountInfoService.startLoginSession( - JBAccountInfoService.LoginMode.AUTO, null, loginMetadata) - - loginSession!!.onCompleted().thenAccept(Consumer { - SettingsSyncEvents.getInstance().fireLoginStateChanged() - }) + cont.invokeOnCancellation { + cont.resume(null) + } + accountInfoService + .startLoginSession(JBAccountInfoService.LoginMode.AUTO, null, loginMetadata) + .onCompleted() + .exceptionally { exc -> + if (exc is CancellationException) { + LOG.warn("Login cancelled") + } + else { + LOG.warn("Login failed", exc) + } + cont.resume(null) + null + }.thenApply { loginResult -> + val result: SettingsSyncUserData? = when (loginResult) { + is JBAccountInfoService.LoginResult.LoginSuccessful -> { + fromJBAData(loginResult.jbaUser) + } + is JBAccountInfoService.LoginResult.LoginFailed -> { + LOG.warn("Login failed: ${loginResult.errorMessage}") + null + } + else -> { + LOG.warn("Unknown login result: $loginResult") + null + } + } + cont.resume(result) + } } catch (e: Throwable) { LOG.error(e) - SettingsSyncEvents.getInstance().fireLoginStateChanged() + cont.resumeWithException(e) } } } } - override fun isLoginAvailable(): Boolean = getAccountInfoService() != null - fun invalidateJBA(idToken: String) { if (invalidatedIdToken == idToken) return diff --git a/plugins/settings-sync/jba/test/com/intellij/settingsSync/SettingsSyncAuthTest.kt b/plugins/settings-sync/jba/test/com/intellij/settingsSync/SettingsSyncAuthTest.kt index 678ff0435ce4..36ff5f302d23 100644 --- a/plugins/settings-sync/jba/test/com/intellij/settingsSync/SettingsSyncAuthTest.kt +++ b/plugins/settings-sync/jba/test/com/intellij/settingsSync/SettingsSyncAuthTest.kt @@ -2,6 +2,7 @@ package com.intellij.settingsSync import com.intellij.idea.TestFor import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.progress.runBlockingCancellable import com.intellij.settingsSync.auth.SettingsSyncAuthService import com.intellij.settingsSync.jba.CloudConfigServerCommunicator import com.intellij.settingsSync.jba.CloudConfigVersionContext @@ -72,11 +73,14 @@ internal class SettingsSyncAuthTest : BasePlatformTestCase() { communicator.checkServerState() verify(authServiceSpy, times(1)).invalidateJBA("OLD-ID-TOKEN") - assertFalse(authServiceSpy.isLoggedIn()) + + TODO("assertFalse(authServiceSpy.isLoggedIn())") // User updates JBA details `when`(accountInfoService.idToken).thenReturn("NEW-ID-TOKEN") - authServiceSpy.login() + runBlockingCancellable { + authServiceSpy.login(null) + } // Check that userId was updated in communicator communicator.checkServerState() @@ -115,7 +119,7 @@ internal class SettingsSyncAuthTest : BasePlatformTestCase() { } communicator.checkServerState() - assertFalse(authServiceSpy.isLoggedIn()) + TODO("assertFalse(authServiceSpy.isLoggedIn())") assertFalse(SettingsSyncSettings.getInstance().syncEnabled) } } \ No newline at end of file diff --git a/plugins/settings-sync/jba/test/com/intellij/settingsSync/SettingsSyncTestAuthService.kt b/plugins/settings-sync/jba/test/com/intellij/settingsSync/SettingsSyncTestAuthService.kt index a93e10a34e32..a7bd48bc7f52 100644 --- a/plugins/settings-sync/jba/test/com/intellij/settingsSync/SettingsSyncTestAuthService.kt +++ b/plugins/settings-sync/jba/test/com/intellij/settingsSync/SettingsSyncTestAuthService.kt @@ -4,13 +4,14 @@ import com.intellij.settingsSync.auth.SettingsSyncAuthService import com.intellij.settingsSync.communicator.SettingsSyncUserData import com.intellij.settingsSync.jba.auth.DummyJBAccountInfoService import com.intellij.ui.JBAccountInfoService +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred +import java.awt.Component +import javax.swing.Icon internal class SettingsSyncTestAuthService : SettingsSyncAuthService { - override fun isLoggedIn(): Boolean { - return true - } - override fun getUserData(): SettingsSyncUserData { + override fun getUserData(userId: String): SettingsSyncUserData { val id = System.getenv("SETTINGS_SYNC_TEST_ID") val loginName = "testLogin" val email = "testEmail@example.com" @@ -18,6 +19,10 @@ internal class SettingsSyncTestAuthService : SettingsSyncAuthService { return SettingsSyncUserData(loginName, email) } + override fun getAvailableUserAccounts(): List { + TODO("Not yet implemented") + } + fun getAccountInfoService(): JBAccountInfoService { return DummyJBAccountInfoService } @@ -27,11 +32,12 @@ internal class SettingsSyncTestAuthService : SettingsSyncAuthService { override val providerCode: String get() = TODO("Not yet implemented") + override val providerName: String + get() = TODO("Not yet implemented") + override val icon: Icon? + get() = TODO("Not yet implemented") - override fun login() { - } - - override fun isLoginAvailable(): Boolean { - return false + override suspend fun login(parentComponent: Component?) : SettingsSyncUserData? { + return null } } \ No newline at end of file diff --git a/plugins/settings-sync/resources/META-INF/plugin.xml b/plugins/settings-sync/resources/META-INF/plugin.xml index b42ad08728e5..af06a378c1aa 100644 --- a/plugins/settings-sync/resources/META-INF/plugin.xml +++ b/plugins/settings-sync/resources/META-INF/plugin.xml @@ -12,6 +12,7 @@ + com.intellij.modules.platform diff --git a/plugins/settings-sync/resources/icons/statusNotRun.svg b/plugins/settings-sync/resources/icons/statusNotRun.svg new file mode 100644 index 000000000000..e4afe6b32a45 --- /dev/null +++ b/plugins/settings-sync/resources/icons/statusNotRun.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/settings-sync/resources/icons/statusNotRun_dark.svg b/plugins/settings-sync/resources/icons/statusNotRun_dark.svg new file mode 100644 index 000000000000..84db828b1ba9 --- /dev/null +++ b/plugins/settings-sync/resources/icons/statusNotRun_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/settings-sync/resources/messages/SettingsSyncBundle.properties b/plugins/settings-sync/resources/messages/SettingsSyncBundle.properties index 06979ae96d20..dd481b834db3 100644 --- a/plugins/settings-sync/resources/messages/SettingsSyncBundle.properties +++ b/plugins/settings-sync/resources/messages/SettingsSyncBundle.properties @@ -41,16 +41,28 @@ config.button.login=Log in with JetBrains Account\u2026 # config.button.logout=Log Out... config.button.enable=Enable Backup and Sync\u2026 config.button.disable=Disable Backup and Sync\u2026 -enable.dialog.select.what.to.sync=Select what settings to sync: +enable.dialog.select.what.to.sync=Settings to sync enable.dialog.enable.sync.action=Enable Backup and Sync +enable.dialog.source.option.title=Choose Settings Source +enable.dialog.source.option.text=There already are existing settings on this account. Please choose which settings you would like to use as the basis for sync going forward. enable.dialog.sync.local.settings=Push Settings to Account +enable.dialog.sync.local.settings.option=Use the local settings and upload them to your account storage +enable.dialog.sync.local.settings.text=Settings saved on your account will be overwritten by the local ones enable.dialog.get.settings.from.account=Get Settings from Account -enable.sync.check.server.data.progress=Checking Server Data\u2026 +enable.dialog.get.settings.from.account.option=Use the settings from your account storage +enable.dialog.get.settings.from.account.text=Your local settings will be overwritten by the settings on your account +enable.dialog.change=Change +enable.sync.check.server.data.progress=Checking server data\u2026 enable.sync.get.from.server.progress=Getting Settings from Server\u2026 enable.sync.push.to.server.progress=Pushing Settings to Server\u2026 -disable.dialog.title=Disable Backup and Sync? -disable.dialog.text=The settings will not be synced anymore -disable.dialog.remove.data.box=Remove data from JB account and disable for all IDEs +enable.sync.add.account=Add Account +enable.sync.choose.data.provider.title=Choose Provider +enable.sync.choose.data.provider.text=Please select a provider that will store and process your data +disable.dialog.title=Confirm disabling Settings Sync +# {0} - account type, i.e. JetBrains or Google +disable.dialog.text=This will disable Settings Sync for this installation,\nleaving the data stored on the {0} account intact.\nThis change will take effect immediately. +# {0} - account type, i.e. JetBrains or Google +disable.dialog.remove.data.box=Remove data from {0} account and disable for all IDEs disable.dialog.disable.button=Disable disable.remove.data.title=Removing Server Data\u2026 disable.remove.data.failure=Unable to remove data from server @@ -66,6 +78,7 @@ subcategory.config.link=Configure settings.category.ui.editor.font=Editor font settings.sync.info.message=Sync UI, Code and System settings, Keymaps, Plugins, and Tools. +settings.sync.select.provider.message=To enable Backup and Sync, select a provider that will store and process your data sync.restart.notification.title=Restart required after syncing settings # {0} - count of plugins, {1} - coma separated list of plugins (max two plugins) @@ -83,12 +96,14 @@ sync.notification.restart.message.list.entry.plugin.disable=Disable {0,choice,1# sync.restart.notification.submessage.plugins={0} plugin(s): {1}\u2026 # {0} - IDE name, i.e. Android Studio, MPS, etc. sync.restart.notification.action=Restart {0} -sync.status.enabled=Backup and Sync enabled +sync.status.enabled=Backup and Sync enabled for sync.status.disabled=Backup and Sync disabled sync.status.failed=Sync failed. sync.status.login.message=Login to enable Backup and Sync -# {0} is last sync time (human-readable), {1} is a user name, for example: "Last synced 5 minutes ago for John.Doe" -sync.status.last.sync.message=Last synced {0} for {1}. +# {0} is last sync time (human-readable), after (in a different component) goes a user-name, for example: "Last synced 5 minutes ago for John.Doe" +sync.status.last.sync.message=Synced {0} for +sync.status.disabled.message=Disabled for +sync.login.message=Logging in\u2026 # The full (original) message is "..., install JetBrains Marketplace Licensing Support" # but the plugin name is rendered in a separate component and doesn't need to be translated sync.status.login.not.available=To enable Backup and Sync, install diff --git a/plugins/settings-sync/src/com/intellij/settingsSync/AbstractServerCommunicator.kt b/plugins/settings-sync/src/com/intellij/settingsSync/AbstractServerCommunicator.kt index c3996857f8ce..6c3a78878f06 100644 --- a/plugins/settings-sync/src/com/intellij/settingsSync/AbstractServerCommunicator.kt +++ b/plugins/settings-sync/src/com/intellij/settingsSync/AbstractServerCommunicator.kt @@ -15,13 +15,75 @@ abstract class AbstractServerCommunicator() : SettingsSyncRemoteCommunicator { private val LOG = logger() } + private var myTemporary = false + + override fun setTemporary(isTemporary: Boolean) { + myTemporary = isTemporary + } + + /** + * called when a request is successful, as an opposite to handleRemoteError + */ + protected open fun requestSuccessful() {} + + /** + * Handles errors that occur during remote operations by mapping the given `Throwable` to a meaningful error message. + * + * @param e The exception or error that occurred during a remote operation. + * @return A string describing the error in a human-readable format, suitable for logging or display. + */ + protected abstract fun handleRemoteError(e: Throwable): String + + /** + * Reads the content of a file from the given file path. + * + * @param filePath The path of the file to be read. + * @return A pair containing: + * - An InputStream of the file content if the operation is successful, otherwise null. + * - A server version identifier associated with the file, or null if unavailable. + * @throws IOException If an I/O error occurs during file reading. + */ + @Throws(IOException::class) + protected abstract fun readFileInternal(filePath: String): Pair + + /** + * Writes the content to a specified file, potentially associated with a particular version ID. + * + * @param filePath The path to the file where the content will be written. + * @param versionId An optional version identifier for the file. If provided, the version ID must match + * the version on server; otherwise, an InvalidVersionIdException may occur. + * @param content The content to be written to the file, provided as an InputStream. + * @return The version ID of the file after the content is written, or null if the operation does not result + * in an identifiable version. + * @throws IOException If an I/O error occurs during the writing process. + * @throws InvalidVersionIdException If the provided(expected) version ID doesn't match the actual remote one. + */ + @Throws(IOException::class, InvalidVersionIdException::class) + protected abstract fun writeFileInternal(filePath: String, versionId: String?, content: InputStream): String? + + /** + * Fetches the latest version identifier for the file at the specified file path. + * + * This version is compared against SettingsSyncLocalSettings.getKnownAndAppliedServerId + * to check whether settings version on server is different from the local one. + * + * @param filePath The path to the file whose latest version is to be retrieved. + * @return The latest version identifier of the file, or null if no version information is available. + * @throws IOException If an I/O error occurs while attempting to retrieve the version information. + */ + @Throws(IOException::class) + protected abstract fun getLatestVersion(filePath: String) : String? + + @Throws(IOException::class) + protected abstract fun deleteFileInternal(filePath: String) + @VisibleForTesting @Throws(IOException::class, SecurityException::class) protected fun currentSnapshotFilePath(): Pair? { try { val crossIdeSyncEnabled = isFileExists(CROSS_IDE_SYNC_MARKER_FILE) - if (crossIdeSyncEnabled != SettingsSyncLocalSettings.getInstance().isCrossIdeSyncEnabled) { + if (!myTemporary && crossIdeSyncEnabled != SettingsSyncLocalSettings.getInstance().isCrossIdeSyncEnabled) { LOG.info("Cross-IDE sync status on server is: ${enabledOrDisabled(crossIdeSyncEnabled)}. Updating local settings with it.") SettingsSyncLocalSettings.getInstance().isCrossIdeSyncEnabled = crossIdeSyncEnabled } @@ -63,7 +125,7 @@ abstract class AbstractServerCommunicator() : SettingsSyncRemoteCommunicator { if (force) { // get the latest server version: pushing with it will overwrite the file in any case versionToPush = getLatestVersion(snapshotFilePath) - writeFileInternal(snapshotFilePath, null, inputStream) + writeFileInternal(snapshotFilePath, versionToPush, inputStream) } else { if (knownServerVersion != null) { @@ -172,22 +234,6 @@ abstract class AbstractServerCommunicator() : SettingsSyncRemoteCommunicator { } } - protected abstract fun requestSuccessful() - - protected abstract fun handleRemoteError(e: Throwable): String - - @Throws(IOException::class) - protected abstract fun readFileInternal(snapshotFilePath: String): Pair - - @Throws(IOException::class, InvalidVersionIdException::class) - protected abstract fun writeFileInternal(filePath: String, versionId: String?, content: InputStream): String? - - @Throws(IOException::class) - protected abstract fun getLatestVersion(filePath: String) : String? - - @Throws(IOException::class) - protected abstract fun deleteFileInternal(filePath: String) - override fun createFile(filePath: String, content: String) { writeFileInternal(filePath, null, content.byteInputStream()) } diff --git a/plugins/settings-sync/src/com/intellij/settingsSync/SettingsSyncBridge.kt b/plugins/settings-sync/src/com/intellij/settingsSync/SettingsSyncBridge.kt index 0238681343de..1ba2b3b68c97 100644 --- a/plugins/settings-sync/src/com/intellij/settingsSync/SettingsSyncBridge.kt +++ b/plugins/settings-sync/src/com/intellij/settingsSync/SettingsSyncBridge.kt @@ -34,7 +34,7 @@ class SettingsSyncBridge( private val pendingEvents = ContainerUtil.createConcurrentList() private val remoteCommunicator: SettingsSyncRemoteCommunicator - get() = RemoteCommunicatorHolder.getRemoteCommunicator() + get() = RemoteCommunicatorHolder.getRemoteCommunicator() ?: DummyCommunicator @Volatile private var queueJob: Job? = null @@ -73,6 +73,7 @@ class SettingsSyncBridge( } } while (!locked) + LOG.info("Lock obtained for exclusive event") processExclusiveEvent(event) } catch (th: Throwable) { @@ -83,6 +84,7 @@ class SettingsSyncBridge( if (!eventsLock.compareAndSet(true, false)) { LOG.error("eventsLock already unlocked by someone else!!!") } + LOG.info("Lock released for exclusive event") } pendingExclusiveEvents.remove(event) } @@ -563,11 +565,54 @@ class SettingsSyncBridge( } } + + @TestOnly internal fun stop() { stopSyncingAndRollback(null, null) } + private object DummyCommunicator : SettingsSyncRemoteCommunicator { + override val userId: String + get() = "" + + override fun setTemporary(isTemporary: Boolean) { + // do nothing + } + + override fun checkServerState(): ServerState { + val errorMsg = "Cannot check server state - no communicator provided" + LOG.info(errorMsg) + return ServerState.Error(errorMsg) + } + + override fun receiveUpdates(): UpdateResult { + val errorMsg = "Cannot received updates - no communicator provided" + LOG.info(errorMsg) + return UpdateResult.Error(errorMsg) + } + + override fun push(snapshot: SettingsSnapshot, force: Boolean, expectedServerVersionId: String?): SettingsSyncPushResult { + val errorMsg = "Cannot push - no communicator provided" + LOG.info(errorMsg) + return SettingsSyncPushResult.Error(errorMsg) + } + + override fun createFile(filePath: String, content: String) { + LOG.info("Cannot create file '$filePath' - no communicator provided") + } + + override fun deleteFile(filePath: String) { + LOG.info("Cannot delete file '$filePath' - no communicator provided") + } + + override fun isFileExists(filePath: String): Boolean { + LOG.info("Cannot check if file '$filePath' exists - no communicator provided") + return false; + } + } + + companion object { private val LOG = logger() } diff --git a/plugins/settings-sync/src/com/intellij/settingsSync/SettingsSyncLocalSettings.kt b/plugins/settings-sync/src/com/intellij/settingsSync/SettingsSyncLocalSettings.kt index c205a3e27461..8dcef7ddf91d 100644 --- a/plugins/settings-sync/src/com/intellij/settingsSync/SettingsSyncLocalSettings.kt +++ b/plugins/settings-sync/src/com/intellij/settingsSync/SettingsSyncLocalSettings.kt @@ -9,6 +9,8 @@ interface SettingsSyncLocalState { val applicationId: UUID var knownAndAppliedServerId: String? var isCrossIdeSyncEnabled: Boolean + var userId: String? + var providerCode: String? } @State(name = "SettingsSyncLocalSettings", storages = [Storage("settingsSyncLocal.xml", roamingType = RoamingType.DISABLED)]) @@ -23,12 +25,16 @@ class SettingsSyncLocalSettings : SimplePersistentStateComponent ideMediator.getInitialSnapshot(appConfigPath, currentSnapshot) }) diff --git a/plugins/settings-sync/src/com/intellij/settingsSync/SettingsSyncRemoteCommunicator.kt b/plugins/settings-sync/src/com/intellij/settingsSync/SettingsSyncRemoteCommunicator.kt index 83776f58c2cc..ca8871ca16d2 100644 --- a/plugins/settings-sync/src/com/intellij/settingsSync/SettingsSyncRemoteCommunicator.kt +++ b/plugins/settings-sync/src/com/intellij/settingsSync/SettingsSyncRemoteCommunicator.kt @@ -18,6 +18,19 @@ const val SETTINGS_SYNC_SNAPSHOT_ZIP = "$SETTINGS_SYNC_SNAPSHOT.zip" @ApiStatus.Internal interface SettingsSyncRemoteCommunicator { + /** + * userId used within the communicator + */ + val userId: String + + + /** + * Indicates whether the communicator is temporary (i.e. used to check a remote state before enabling) + * and shouldn't update configuration values + */ + fun setTemporary(isTemporary: Boolean) + + /** * checks the current state of the user's data in the cloud. */ diff --git a/plugins/settings-sync/src/com/intellij/settingsSync/SettingsSyncSettings.kt b/plugins/settings-sync/src/com/intellij/settingsSync/SettingsSyncSettings.kt index 499172106fe8..37fbce6baab1 100644 --- a/plugins/settings-sync/src/com/intellij/settingsSync/SettingsSyncSettings.kt +++ b/plugins/settings-sync/src/com/intellij/settingsSync/SettingsSyncSettings.kt @@ -60,6 +60,12 @@ class SettingsSyncSettings : SettingsSyncState, SerializablePersistentStateCompo override val disabledSubcategories: Map> get() = state.disabledSubcategories + fun updateCategories(disabledCategories: List, disabledSubcategories: Map>) { + updateState { + it.withDisabledCategories(disabledCategories).withDisabledSubcategories(disabledSubcategories) + } + } + fun applyFromState(state: SettingsSyncState) { updateState { State(state.disabledCategories, state.disabledSubcategories, state.migrationFromOldStorageChecked, state.syncEnabled) @@ -78,7 +84,7 @@ class SettingsSyncSettings : SettingsSyncState, SerializablePersistentStateCompo return State(disabledCategories, disabledSubcategories, checked, syncEnabled) } - private fun withDisabledCategories(newCategories: List): State { + internal fun withDisabledCategories(newCategories: List): State { return State(newCategories, disabledSubcategories, migrationFromOldStorageChecked, syncEnabled) } @@ -95,7 +101,7 @@ class SettingsSyncSettings : SettingsSyncState, SerializablePersistentStateCompo } - private fun withDisabledSubcategories(newSubcategoriesMap: Map>): State { + internal fun withDisabledSubcategories(newSubcategoriesMap: Map>): State { return State(disabledCategories, newSubcategoriesMap, migrationFromOldStorageChecked, syncEnabled) } diff --git a/plugins/settings-sync/src/com/intellij/settingsSync/SettingsSyncUpdateChecker.kt b/plugins/settings-sync/src/com/intellij/settingsSync/SettingsSyncUpdateChecker.kt index 4ad2c4e49b1a..4f1afb9c205e 100644 --- a/plugins/settings-sync/src/com/intellij/settingsSync/SettingsSyncUpdateChecker.kt +++ b/plugins/settings-sync/src/com/intellij/settingsSync/SettingsSyncUpdateChecker.kt @@ -15,7 +15,11 @@ class SettingsSyncUpdateChecker() { @RequiresBackgroundThread fun scheduleUpdateFromServer() : UpdateResult { - val updateResult = RemoteCommunicatorHolder.getRemoteCommunicator().receiveUpdates() + val updateResult = RemoteCommunicatorHolder.getRemoteCommunicator()?.receiveUpdates() ?: run { + val errorMsg = "Cannot get update from server - no communicator provider" + LOG.info(errorMsg) + return UpdateResult.Error(errorMsg) + } when(updateResult) { is UpdateResult.Success -> { val snapshot = updateResult.settingsSnapshot diff --git a/plugins/settings-sync/src/com/intellij/settingsSync/SettingsSynchronizer.kt b/plugins/settings-sync/src/com/intellij/settingsSync/SettingsSynchronizer.kt index c6cb642258fe..0084e4ece58c 100644 --- a/plugins/settings-sync/src/com/intellij/settingsSync/SettingsSynchronizer.kt +++ b/plugins/settings-sync/src/com/intellij/settingsSync/SettingsSynchronizer.kt @@ -13,6 +13,7 @@ import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.progress.blockingContext import com.intellij.openapi.util.registry.Registry import com.intellij.openapi.wm.IdeFrame +import com.intellij.settingsSync.communicator.RemoteCommunicatorHolder import com.intellij.settingsSync.migration.migrateIfNeeded import com.intellij.settingsSync.statistics.SettingsSyncEventsStatistics import com.intellij.util.concurrency.AppExecutorUtil @@ -50,6 +51,9 @@ private class SettingsSynchronizerApplicationInitializedListener : ApplicationAc } } } + + setProviderCodeAndUserId() + serviceAsync().addListener(settingsSyncEventListener) if (isSettingsSyncEnabledInSettings()) { @@ -75,17 +79,28 @@ private class SettingsSynchronizerApplicationInitializedListener : ApplicationAc } } } -} -private suspend fun initializeSyncing(initMode: SettingsSyncBridge.InitMode, settingsSyncEventListener: SettingsSyncEventListener) { - LOG.info("Initializing settings sync. Mode: $initMode") - val settingsSyncMain = serviceAsync() - blockingContext { - settingsSyncMain.controls.bridge.initialize(initMode) - val settingsSyncEvents = SettingsSyncEvents.getInstance() - settingsSyncEvents.addListener(settingsSyncEventListener) - settingsSyncEvents.fireSettingsChanged(SyncSettingsEvent.SyncRequest) - LocalHostNameProvider.initialize() + private suspend fun initializeSyncing(initMode: SettingsSyncBridge.InitMode, settingsSyncEventListener: SettingsSyncEventListener) { + LOG.info("Initializing settings sync. Mode: $initMode") + val settingsSyncMain = serviceAsync() + blockingContext { + settingsSyncMain.controls.bridge.initialize(initMode) + val settingsSyncEvents = SettingsSyncEvents.getInstance() + settingsSyncEvents.addListener(settingsSyncEventListener) + settingsSyncEvents.fireSettingsChanged(SyncSettingsEvent.SyncRequest) + LocalHostNameProvider.initialize() + } + } + + private fun setProviderCodeAndUserId() { + if (!SettingsSyncSettings.getInstance().syncEnabled) + return + if (SettingsSyncLocalSettings.getInstance().providerCode.isNullOrBlank()) { + SettingsSyncLocalSettings.getInstance().providerCode = RemoteCommunicatorHolder.DEFAULT_PROVIDER_CODE + } + if (SettingsSyncLocalSettings.getInstance().userId.isNullOrBlank()) { + SettingsSyncLocalSettings.getInstance().userId = RemoteCommunicatorHolder.DEFAULT_USER_ID + } } } diff --git a/plugins/settings-sync/src/com/intellij/settingsSync/auth/SettingsSyncAuthService.kt b/plugins/settings-sync/src/com/intellij/settingsSync/auth/SettingsSyncAuthService.kt index c9c8596e1e46..5e988c76149b 100644 --- a/plugins/settings-sync/src/com/intellij/settingsSync/auth/SettingsSyncAuthService.kt +++ b/plugins/settings-sync/src/com/intellij/settingsSync/auth/SettingsSyncAuthService.kt @@ -1,6 +1,9 @@ package com.intellij.settingsSync.auth import com.intellij.settingsSync.communicator.SettingsSyncUserData +import kotlinx.coroutines.Deferred +import java.awt.Component +import javax.swing.Icon interface SettingsSyncAuthService { /** @@ -10,22 +13,25 @@ interface SettingsSyncAuthService { val providerCode: String /** - * Starts the login procedure + * Free-form name of the provider */ - fun login() + val providerName: String /** - * Whether the user has logged in + * provider icon */ - fun isLoggedIn(): Boolean + val icon: Icon? /** + * Starts the login procedure (if necessary) and returns the Deferred of the logged-in user + */ + suspend fun login(parentComponent: Component?) : SettingsSyncUserData? + + /** + * Data of the current user. If there's no user, return null * This data is used for in the local git repo as well as UI (if necessary) */ - fun getUserData(): SettingsSyncUserData + fun getUserData(userId: String): SettingsSyncUserData? - /** - * Indicates if it's currently possible to log in (i.e. all necessary plugins are present and enabled, etc), given the current state of IDE. - */ - fun isLoginAvailable(): Boolean + fun getAvailableUserAccounts(): List } \ No newline at end of file diff --git a/plugins/settings-sync/src/com/intellij/settingsSync/communicator/RemoteCommunicatorHolder.kt b/plugins/settings-sync/src/com/intellij/settingsSync/communicator/RemoteCommunicatorHolder.kt index 88ecc3e55bd0..2415603e7779 100644 --- a/plugins/settings-sync/src/com/intellij/settingsSync/communicator/RemoteCommunicatorHolder.kt +++ b/plugins/settings-sync/src/com/intellij/settingsSync/communicator/RemoteCommunicatorHolder.kt @@ -1,95 +1,90 @@ package com.intellij.settingsSync.communicator import com.intellij.openapi.diagnostic.logger -import com.intellij.settingsSync.ServerState -import com.intellij.settingsSync.SettingsSnapshot -import com.intellij.settingsSync.SettingsSyncPushResult -import com.intellij.settingsSync.SettingsSyncRemoteCommunicator -import com.intellij.settingsSync.UpdateResult -import com.intellij.settingsSync.auth.SettingsSyncAuthService -import com.intellij.util.concurrency.SynchronizedClearableLazy +import com.intellij.settingsSync.* +import com.intellij.util.resettableLazy +import org.jetbrains.annotations.ApiStatus -object RemoteCommunicatorHolder { +@ApiStatus.Internal +object RemoteCommunicatorHolder : SettingsSyncEventListener { + private val logger = logger() + const val DEFAULT_PROVIDER_CODE = "jba" + const val DEFAULT_USER_ID = "jba" - private val communicatorInternalLazy = SynchronizedClearableLazy { - getCurrentCommunicator().also { - logger().warn("Initializing remote communicator: $it") + // pair userId:remoteCommunicator + private val communicatorLazy = resettableLazy { + createRemoteCommunicator() + } + + fun getRemoteCommunicator(): SettingsSyncRemoteCommunicator? = communicatorLazy.value ?: createRemoteCommunicator() + + fun getAuthService() = getCurrentProvider()?.authService + + fun isAvailable() = communicatorLazy.value != null + + fun invalidateCommunicator() = communicatorLazy.reset().also { + logger.warn("Invalidating remote communicator") + } + + fun getCurrentUserData() : SettingsSyncUserData? { + val userId = SettingsSyncLocalSettings.getInstance().userId ?: run { + logger.warn("No current userId. Returning null user data") + return null } + val provider: SettingsSyncCommunicatorProvider = getCurrentProvider() ?: run { + logger.warn("No active provider. Returning null user data") + return null + } + return provider.authService.getUserData(userId) } - fun getRemoteCommunicator(): SettingsSyncRemoteCommunicator = communicatorInternalLazy.value - fun getAuthService() = getProvider().authService - fun isAvailable() = communicatorInternalLazy.isInitialized() - fun invalidateCommunicator() = communicatorInternalLazy.drop().also { - logger().warn("Invalidating remote communicator: $it") + override fun loginStateChanged() { } + fun createRemoteCommunicator(provider: SettingsSyncCommunicatorProvider, userId: String): SettingsSyncRemoteCommunicator? { + return provider.createCommunicator(userId) + } - private fun getProvider() : SettingsSyncCommunicatorProvider { + private fun createRemoteCommunicator(): SettingsSyncRemoteCommunicator? { + val provider: SettingsSyncCommunicatorProvider = getCurrentProvider() ?: run { + logger.warn("Attempting to create remote communicator without active provider") + return null + } + val userId = SettingsSyncLocalSettings.getInstance().userId ?: run { + logger.warn("Empty current userId. Communicator will not be created.") + return null + } + + val currentUserData = provider.authService.getUserData(userId) ?: run { + logger.warn("Empty current user data. Communicator will not be created.") + return null + } + val communicator: SettingsSyncRemoteCommunicator = provider.createCommunicator(currentUserData.id) ?: run { + logger.warn("Provider '${provider.providerCode}' returned empty communicator") + return null + } + return communicator + } + + fun getAvailableProviders(): List { val extensionList = SettingsSyncCommunicatorProvider.PROVIDER_EP.extensionList - return extensionList.firstOrNull() ?: DummyProvider + return extensionList } - private fun getCurrentCommunicator() : SettingsSyncRemoteCommunicator { - return getProvider().createCommunicator() ?: DummyCommunicator + fun getDefaultProvider(): SettingsSyncCommunicatorProvider { + return getProvider(DEFAULT_PROVIDER_CODE)!! } - internal object DummyProvider: SettingsSyncCommunicatorProvider { - override val providerCode: String - get() = "dummy" - override val authService: SettingsSyncAuthService - get() = DummyAuthProvider - - override fun createCommunicator(): SettingsSyncRemoteCommunicator? { - return DummyCommunicator - } - + fun getProvider(providerCode: String): SettingsSyncCommunicatorProvider? { + return getAvailableProviders().find { it.providerCode == providerCode } } - internal object DummyAuthProvider: SettingsSyncAuthService { - override val providerCode: String - get() = "dummy" - - override fun login() { - TODO("Not yet implemented") - } - - override fun isLoggedIn(): Boolean { - return false - } - - override fun getUserData(): SettingsSyncUserData { - return SettingsSyncUserData.EMPTY - } - - override fun isLoginAvailable(): Boolean { - return true - } + fun getAvailableUserAccounts(): List { + return getAvailableProviders().flatMap { it.authService.getAvailableUserAccounts() } } - internal object DummyCommunicator: SettingsSyncRemoteCommunicator { - override fun checkServerState(): ServerState { - TODO("Not yet implemented") - } - - override fun receiveUpdates(): UpdateResult { - TODO("Not yet implemented") - } - - override fun push(snapshot: SettingsSnapshot, force: Boolean, expectedServerVersionId: String?): SettingsSyncPushResult { - TODO("Not yet implemented") - } - - override fun createFile(filePath: String, content: String) { - TODO("Not yet implemented") - } - - override fun deleteFile(filePath: String) { - TODO("Not yet implemented") - } - - override fun isFileExists(filePath: String): Boolean { - TODO("Not yet implemented") - } + private fun getCurrentProvider(): SettingsSyncCommunicatorProvider? { + val providerCode = SettingsSyncLocalSettings.getInstance().providerCode ?: return null + return getProvider(providerCode) } } \ No newline at end of file diff --git a/plugins/settings-sync/src/com/intellij/settingsSync/communicator/SettingsSyncCommunicatorProvider.kt b/plugins/settings-sync/src/com/intellij/settingsSync/communicator/SettingsSyncCommunicatorProvider.kt index 2a20fb1bb802..2c11af1c41ea 100644 --- a/plugins/settings-sync/src/com/intellij/settingsSync/communicator/SettingsSyncCommunicatorProvider.kt +++ b/plugins/settings-sync/src/com/intellij/settingsSync/communicator/SettingsSyncCommunicatorProvider.kt @@ -19,7 +19,7 @@ interface SettingsSyncCommunicatorProvider { /** * Creates a communicator (using the login data from authService) */ - fun createCommunicator(): SettingsSyncRemoteCommunicator? + fun createCommunicator(userId: String): SettingsSyncRemoteCommunicator? companion object { @JvmField @@ -28,10 +28,9 @@ interface SettingsSyncCommunicatorProvider { } data class SettingsSyncUserData( - val name: String?, - val email: String?, -) { - companion object { - val EMPTY = SettingsSyncUserData(null, null) - } -} \ No newline at end of file + val id: String, + val providerCode: String, + val name: String? = null, + val email: String? = null, + val printableName: String? = null +) \ No newline at end of file diff --git a/plugins/settings-sync/src/com/intellij/settingsSync/config/SettingsSyncConfigurable.kt b/plugins/settings-sync/src/com/intellij/settingsSync/config/SettingsSyncConfigurable.kt index faa42f9ec631..9f5cb9e04b66 100644 --- a/plugins/settings-sync/src/com/intellij/settingsSync/config/SettingsSyncConfigurable.kt +++ b/plugins/settings-sync/src/com/intellij/settingsSync/config/SettingsSyncConfigurable.kt @@ -1,451 +1,576 @@ package com.intellij.settingsSync.config + import com.intellij.icons.AllIcons -import com.intellij.ide.DataManager -import com.intellij.ide.plugins.InstalledPluginsState -import com.intellij.ide.plugins.PluginManagerConfigurable -import com.intellij.ide.plugins.PluginStateManager -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.ApplicationNamesInfo -import com.intellij.openapi.application.ModalityState -import com.intellij.openapi.application.ex.ApplicationEx -import com.intellij.openapi.application.runInEdt -import com.intellij.openapi.components.impl.stores.stateStore -import com.intellij.openapi.extensions.PluginId +import com.intellij.openapi.application.* +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.observable.properties.AtomicBooleanProperty +import com.intellij.openapi.observable.properties.AtomicProperty +import com.intellij.openapi.observable.util.and +import com.intellij.openapi.observable.util.not import com.intellij.openapi.options.BoundConfigurable import com.intellij.openapi.options.Configurable import com.intellij.openapi.options.ConfigurableProvider -import com.intellij.openapi.options.ex.Settings -import com.intellij.openapi.progress.ProgressIndicator -import com.intellij.openapi.progress.Task -import com.intellij.openapi.ui.DialogPanel -import com.intellij.openapi.ui.Messages +import com.intellij.openapi.ui.* +import com.intellij.platform.ide.progress.ModalTaskOwner +import com.intellij.platform.ide.progress.TaskCancellation +import com.intellij.platform.ide.progress.runWithModalProgressBlocking +import com.intellij.platform.ide.progress.withModalProgress import com.intellij.settingsSync.* import com.intellij.settingsSync.SettingsSyncBundle.message import com.intellij.settingsSync.UpdateResult.* -import com.intellij.settingsSync.auth.SettingsSyncAuthService import com.intellij.settingsSync.communicator.RemoteCommunicatorHolder -//import com.intellij.settingsSync.auth.SettingsSyncAuthService +import com.intellij.settingsSync.communicator.SettingsSyncCommunicatorProvider +import com.intellij.settingsSync.communicator.SettingsSyncUserData +import com.intellij.settingsSync.config.SettingsSyncEnabler.State import com.intellij.settingsSync.statistics.SettingsSyncEventsStatistics -import com.intellij.ui.components.ActionLink -import com.intellij.ui.dsl.builder.BottomGap -import com.intellij.ui.dsl.builder.Cell -import com.intellij.ui.dsl.builder.RightGap -import com.intellij.ui.dsl.builder.panel -import com.intellij.ui.layout.ComponentPredicate -import com.intellij.ui.layout.and -import com.intellij.ui.layout.not +import com.intellij.ui.components.DropDownLink +import com.intellij.ui.dsl.builder.* +import com.intellij.ui.dsl.listCellRenderer.groupedTextListCellRenderer import com.intellij.util.text.DateFormatUtil -import org.jetbrains.annotations.Nls +import com.intellij.util.ui.JBUI +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.awt.event.ItemEvent +import java.util.concurrent.CancellationException import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit -import javax.swing.JButton -import javax.swing.JCheckBox -import javax.swing.JLabel +import javax.swing.* -internal class SettingsSyncConfigurable : BoundConfigurable(message("title.settings.sync")), - SettingsSyncEnabler.Listener, - SettingsSyncStatusTracker.Listener { +internal class SettingsSyncConfigurable(private val coroutineScope: CoroutineScope) : BoundConfigurable(message("title.settings.sync")), + SettingsSyncEnabler.Listener, + SettingsSyncStatusTracker.Listener { + companion object { + private val LOG = logger() + } private lateinit var configPanel: DialogPanel - private lateinit var enableButton: Cell + private lateinit var enableButton: JButton private lateinit var statusLabel: JLabel - - @Volatile - private var marketplacePluginInstalled = false + private lateinit var userDropDownLink: DropDownLink + private lateinit var syncTypeLabel: JEditorPane private val syncEnabler = SettingsSyncEnabler() - private val MARKETPLACE_PLUGIN_ID = PluginId.getId("com.intellij.marketplace") - + private val enabledStatus = AtomicBooleanProperty(false) + private val enableSyncOption = AtomicProperty(InitSyncType.GET_FROM_SERVER) + private val disableSyncOption = AtomicProperty(DisableSyncType.DISABLE) + private val remoteSettingsExist = AtomicBooleanProperty(false) + private val wasUsedBefore = AtomicBooleanProperty(SettingsSyncLocalSettings.getInstance().userId != null) + private val userAccountsList = arrayListOf() + private val syncPanelHolder = SettingsSyncPanelHolder() + private val hasMultipleProviders = AtomicBooleanProperty(RemoteCommunicatorHolder.getAvailableProviders().size > 1) init { syncEnabler.addListener(this) SettingsSyncStatusTracker.getInstance().addListener(this) } - inner class LoggedInPredicate : ComponentPredicate() { - override fun addListener(listener: (Boolean) -> Unit) = - SettingsSyncEvents.getInstance().addListener( - object : SettingsSyncEventListener { - override fun loginStateChanged() { - listener(invoke()) - } - }, - disposable!!) - - override fun invoke() = RemoteCommunicatorHolder.getAuthService().isLoggedIn() - } - - inner class EnabledPredicate : ComponentPredicate() { - override fun addListener(listener: (Boolean) -> Unit) { - SettingsSyncEvents.getInstance().addListener(object : SettingsSyncEventListener { - override fun enabledStateChanged(syncEnabled: Boolean) { - listener(invoke()) - configPanel.reset() - } - }, disposable!!) - } - - override fun invoke() = SettingsSyncSettings.getInstance().syncEnabled - - } - - inner class SyncEnablerRunning : ComponentPredicate() { - private var isRunning = false - - override fun addListener(listener: (Boolean) -> Unit) { - syncEnabler.addListener(object : SettingsSyncEnabler.Listener { - override fun serverRequestStarted() { - updateRunning(listener, true) - } - - override fun serverRequestFinished() { - updateRunning(listener, false) - } - }) - } - - private fun updateRunning(listener: (Boolean) -> Unit, isRunning: Boolean) { - this.isRunning = isRunning - listener(invoke()) - } - - override fun invoke(): Boolean = isRunning - } - - inner class AuthServiceRestartPredicate : ComponentPredicate() { - init { - marketplacePluginInstalled = InstalledPluginsState.getInstance().wasInstalled(MARKETPLACE_PLUGIN_ID) - } - - override fun addListener(listener: (Boolean) -> Unit) { - PluginStateManager.addStateListener { descriptor -> - if (descriptor.pluginId == MARKETPLACE_PLUGIN_ID) { - // InstalledPluginsState.getInstance().wasInstalled(MARKETPLACE_PLUGIN_ID) is still false at that time, - // so we just cache the value - marketplacePluginInstalled = true - listener(marketplacePluginInstalled) - } - } - } - - override fun invoke(): Boolean { - return marketplacePluginInstalled - } - } - override fun createPanel(): DialogPanel { - val syncConfigPanel = SettingsSyncPanelFactory.createCombinedSyncSettingsPanel( - message("configurable.what.to.sync.label"), - SettingsSyncSettings.getInstance(), - SettingsSyncLocalSettings.getInstance(), - ) - val authService = RemoteCommunicatorHolder.getAuthService() - val authAvailable = authService.isLoginAvailable() + val syncConfigPanel = syncPanelHolder.createCombinedSyncSettingsPanel(message("configurable.what.to.sync.label"), + SettingsSyncSettings.getInstance(), + SettingsSyncLocalSettings.getInstance()) + configPanel = panel { - val isSyncEnabled = LoggedInPredicate().and(EnabledPredicate()) - if (settingsRepositoryIsEnabled()) { - row { - label(message("settings.warning.sync.cannot.be.enabled.label")).applyToComponent { - icon = AllIcons.General.Warning + enabledStatus.set(SettingsSyncSettings.getInstance().syncEnabled) + var userProviderHolder: UserProviderHolder? = null + if (SettingsSyncLocalSettings.getInstance().userId != null && SettingsSyncLocalSettings.getInstance().providerCode != null) { + val authService = RemoteCommunicatorHolder.getProvider(SettingsSyncLocalSettings.getInstance().providerCode!!)?.authService + if (authService != null) { + authService.getAvailableUserAccounts().find { + it.id == SettingsSyncLocalSettings.getInstance().userId + }?.apply { + userProviderHolder = toUserProviderHolder(authService.providerName) } - bottomGap(BottomGap.MEDIUM) } } - // authService is not available without restart - if (authAvailable) { - row { - val statusCell = label("") - statusCell - .visibleIf(LoggedInPredicate()) - .enabled(!settingsRepositoryIsEnabled()) - statusLabel = statusCell.component - updateStatusInfo() - label(message("sync.status.login.message")) - .visibleIf(LoggedInPredicate().not()) - .enabled(!settingsRepositoryIsEnabled()) - } - row { - button(message("config.button.login")) { - authService.login() - }.visibleIf(LoggedInPredicate().not()) - .enabled(!settingsRepositoryIsEnabled()) - enableButton = button(message("config.button.enable")) { - syncEnabler.checkServerState() - }.visibleIf(LoggedInPredicate().and(EnabledPredicate().not())) - .enabledIf(SyncEnablerRunning().not()) - .enabled(!settingsRepositoryIsEnabled()) - - button(message("config.button.disable")) { - LoggedInPredicate().and(EnabledPredicate()) - disableSync() - }.visibleIf(isSyncEnabled) - bottomGap(BottomGap.MEDIUM) - } + updateUserAccountsList() + enabledStatus.afterChange { + syncStatusChanged() } - else { - val authServiceRestartPredicate = AuthServiceRestartPredicate() - row { - label(message("sync.status.login.not.available")).gap(RightGap.SMALL) - @Suppress("DialogTitleCapitalization", "HardCodedStringLiteral") - link("JetBrains Marketplace Licensing Support") { - val settings = Settings.KEY.getData(DataManager.getInstance().getDataContext(it.source as ActionLink)) - val pluginManager = settings?.find("preferences.pluginManager") - if (pluginManager is PluginManagerConfigurable) { - settings.select(pluginManager).doWhenDone { - pluginManager.openMarketplaceTab("/organization:JetBrains Marketplace Licensing") + + row { + label(message("settings.sync.info.message")) + }.visibleIf(enabledStatus.not()) + + row { + label(message("settings.sync.select.provider.message")) + }.visibleIf(enabledStatus.not()) + + row { + val availableProviders = RemoteCommunicatorHolder.getAvailableProviders() + availableProviders.forEach { provider -> + button(provider.authService.providerName) { + login(provider, syncConfigPanel) + }.applyToComponent { + icon = provider.authService.icon + } + } + }.visibleIf(wasUsedBefore.not().and(hasMultipleProviders)) + + row { + val defaultProvider = RemoteCommunicatorHolder.getDefaultProvider() + button(message("config.button.login")) { + login(defaultProvider, syncConfigPanel) + } + }.visibleIf(wasUsedBefore.not().and(hasMultipleProviders.not())) + + row { + val label = label("").applyToComponent { + iconTextGap = 6 + }.gap(RightGap.SMALL) + statusLabel = label.component + cell(object: DropDownLink(userProviderHolder, userAccountsList) { + override fun createRenderer(): ListCellRenderer { + return groupedTextListCellRenderer({ + if (it == UserProviderHolder.addAccount) { + message("enable.sync.add.account") + } else { + it.toString() + } + + }, { + it?.separatorString + }) + } + }).onChangedContext { component, context -> + val event = context.event + if (event is ItemEvent && event.item == UserProviderHolder.addAccount) { + val syncTypeDialog = AddAccountDialog(configPanel) + if (syncTypeDialog.showAndGet()) { + val providerCode = syncTypeDialog.providerCode + val provider = RemoteCommunicatorHolder.getProvider(providerCode) ?: return@onChangedContext + component.selectedItem = null + component.text = "" + login(provider, syncConfigPanel) + } + } else { + component.text = component.selectedItem.toString() + } + }.apply { + userDropDownLink = this.component + } + + }.visibleIf(wasUsedBefore) + + row { + val enableButtonCell = button(message("config.button.enable")) { + if (!enabledStatus.get()) { + runWithModalProgressBlocking(ModalTaskOwner.component(configPanel), message("enable.sync.check.server.data.progress")) { + val (userId, userData, providerCode, providerName) = userDropDownLink.selectedItem ?: run { + LOG.warn("No selected user") + return@runWithModalProgressBlocking + } + val provider = RemoteCommunicatorHolder.getProvider(providerCode) ?: run { + LOG.warn("Provider '$providerName' ($providerCode) is not available") + return@runWithModalProgressBlocking + } + val remoteCommunicator = RemoteCommunicatorHolder.createRemoteCommunicator(provider, userId) ?: run { + LOG.warn("Cannot create remote communicator of type '$providerName' ($providerCode)") + return@runWithModalProgressBlocking + } + if (checkServerState(syncPanelHolder, remoteCommunicator)) { + enabledStatus.set(true) + syncStatusChanged() } } + } else { + val syncDisableOption = showDisableSyncDialog() + if (syncDisableOption != DisableSyncType.DONT_DISABLE) { + enabledStatus.set(false) + disableSyncOption.set(syncDisableOption) + syncStatusChanged() + } } - }.visibleIf(authServiceRestartPredicate.not()) - row { - label(message("sync.status.restart.required", ApplicationNamesInfo.getInstance().fullProductName)) - }.visibleIf(authServiceRestartPredicate) - row { - button(message("sync.status.restart.ide.button")) { - val app = ApplicationManager.getApplication() as ApplicationEx - app.restart(true) - } - }.visibleIf(authServiceRestartPredicate) - } - row { - comment(message("settings.sync.info.message"), 80) - .visibleIf(isSyncEnabled.not()) - } - - row { - cell(syncConfigPanel) - .visibleIf(LoggedInPredicate().and(EnabledPredicate())) - .onApply { - syncConfigPanel.apply() - - SettingsSyncEvents.getInstance().fireCategoriesChanged() - SettingsSyncEvents.getInstance().fireSettingsChanged( - SyncSettingsEvent.CrossIdeSyncStateChanged(SettingsSyncLocalSettings.getInstance().isCrossIdeSyncEnabled)) - } - .onReset(syncConfigPanel::reset) - .onIsModified(syncConfigPanel::isModified) - } - } - SettingsSyncEvents.getInstance().addListener( - object : SettingsSyncEventListener { - override fun loginStateChanged() { - if (RemoteCommunicatorHolder.getAuthService().isLoggedIn() && !SettingsSyncSettings.getInstance().syncEnabled) { - syncEnabler.checkServerState() - } - reset() } - }, - disposable!! - ) + enableButton = enableButtonCell.component + }.visibleIf(wasUsedBefore) + + + // settings to sync + group(message("enable.dialog.select.what.to.sync")) { + row { + icon(AllIcons.General.BalloonWarning).applyToComponent { + isOpaque = true + background = JBUI.CurrentTheme.Banner.WARNING_BACKGROUND + border = JBUI.Borders.compound( + JBUI.Borders.customLine(JBUI.CurrentTheme.Banner.WARNING_BORDER_COLOR, 1, 1, 1, 0), + JBUI.Borders.empty(8) + ) + verticalAlignment = SwingConstants.TOP + }.align(AlignY.FILL) + text("", + action = { + val syncTypeDialog = ChangeSyncTypeDialog(configPanel, enableSyncOption.get()) + if (syncTypeDialog.showAndGet()) { + enableSyncOption.set(syncTypeDialog.option) + } + }).applyToComponent { + isOpaque = true + background = JBUI.CurrentTheme.Banner.WARNING_BACKGROUND + border = JBUI.Borders.compound( + JBUI.Borders.customLine(JBUI.CurrentTheme.Banner.WARNING_BORDER_COLOR, 1, 0, 1, 1), + JBUI.Borders.empty(8) + ) + }.align(AlignX.FILL).resizableColumn().also { + syncTypeLabel = it.component + enableSyncOption.afterChange { + updateSyncOptionText() + } + } + cell() + }.layout(RowLayout.PARENT_GRID).topGap(TopGap.SMALL) + .visibleIf(remoteSettingsExist) + + row { + cell(syncConfigPanel) + .onReset(syncConfigPanel::reset) + .onIsModified{ + enabledStatus.get() != SettingsSyncSettings.getInstance().syncEnabled || syncConfigPanel.isModified() + } + .onApply { + with(SettingsSyncLocalSettings.getInstance()) { + userId = userDropDownLink.selectedItem?.userId + providerCode = userDropDownLink.selectedItem?.providerCode + } + if (enabledStatus.get()) { + syncConfigPanel.apply() + } + if (SettingsSyncSettings.getInstance().syncEnabled != enabledStatus.get()) { + if (enabledStatus.get()) { + SettingsSyncSettings.getInstance().syncEnabled = enabledStatus.get() + if (enableSyncOption.get() == InitSyncType.GET_FROM_SERVER) { + syncEnabler.getSettingsFromServer() + } + else { + syncEnabler.pushSettingsToServer() + } + } else { + when (disableSyncOption.get()) { + DisableSyncType.DISABLE_AND_REMOVE_DATA -> { + disableAndRemoveData() + SettingsSyncEventsStatistics.DISABLED_MANUALLY.log( + SettingsSyncEventsStatistics.ManualDisableMethod.DISABLED_AND_REMOVED_DATA_FROM_SERVER) + } + DisableSyncType.DISABLE -> { + SettingsSyncSettings.getInstance().syncEnabled = false + syncStatusChanged() + SettingsSyncEventsStatistics.DISABLED_MANUALLY.log(SettingsSyncEventsStatistics.ManualDisableMethod.DISABLED_ONLY) + } + else -> { + SettingsSyncSettings.getInstance().syncEnabled = false + syncStatusChanged() + SettingsSyncEventsStatistics.DISABLED_MANUALLY.log(SettingsSyncEventsStatistics.ManualDisableMethod.DISABLED_ONLY) + } + } + SettingsSyncSettings.getInstance().syncEnabled = enabledStatus.get() + } + } + } + }.topGap(TopGap.SMALL) + }.visibleIf(enabledStatus) + + // apply necessary changes + } + syncStatusChanged() return configPanel } - private fun settingsRepositoryIsEnabled(): Boolean { - return !SettingsSyncSettings.getInstance().syncEnabled && - (ApplicationManager.getApplication().stateStore.storageManager).streamProvider.let { it.enabled && it.isExclusive } - } - - override fun serverStateCheckFinished(state: UpdateResult) { - when (state) { - NoFileOnServer, FileDeletedFromServer -> showEnableSyncDialog(null, null) - is Success -> showEnableSyncDialog( - state.settingsSnapshot.getState(), - SettingsSyncLocalStateHolder(state.isCrossIdeSyncEnabled), - ) - is Error -> { - if (state != SettingsSyncEnabler.State.CANCELLED) { - showError(message("notification.title.update.error"), state.message) - } - } - } - } - - override fun updateFromServerFinished(result: UpdateResult) { - when (result) { - is Success -> { - reset() - SettingsSyncSettings.getInstance().syncEnabled = true - } - NoFileOnServer, FileDeletedFromServer -> { - showError(message("notification.title.update.error"), message("notification.title.update.no.such.file")) - } - is Error -> { - showError(message("notification.title.update.error"), result.message) - } - } - updateStatusInfo() - } - - private fun showEnableSyncDialog(remoteSettings: SettingsSyncState?, remoteSyncScopeSettings: SettingsSyncLocalStateHolder?) { - val dialog = EnableSettingsSyncDialog(configPanel, remoteSettings, remoteSyncScopeSettings) - - dialog.show() - - val dialogResult = dialog.getResult() - - if (dialogResult != null) { - when (dialogResult) { - EnableSettingsSyncDialog.Result.GET_FROM_SERVER -> { - syncEnabler.getSettingsFromServer(dialog.syncSettings) - - SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.GET_FROM_SERVER) - } - - EnableSettingsSyncDialog.Result.PUSH_LOCAL -> { - SettingsSyncSettings.getInstance().syncEnabled = true - - syncEnabler.pushSettingsToServer() - - if (remoteSettings != null) { - SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.PUSH_LOCAL) - } - else { - SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.PUSH_LOCAL_WAS_ONLY_WAY) - } - } - } - } - else { - SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.CANCELED) - } - - reset() - configPanel.reset() - } - - companion object DisableResult { - const val RESULT_CANCEL = 0 - const val RESULT_REMOVE_DATA_AND_DISABLE = 1 - const val RESULT_DISABLE = 2 - } - - private fun disableSync() { + private fun showDisableSyncDialog(): Int { @Suppress("DialogTitleCapitalization") - val result = Messages.showCheckboxMessageDialog( // TODO: Use AlertMessage instead - message("disable.dialog.text"), + val providerName = userDropDownLink.selectedItem?.providerName ?: "" + return Messages.showCheckboxMessageDialog( // TODO: Use AlertMessage instead + message("disable.dialog.text", providerName), message("disable.dialog.title"), arrayOf(Messages.getCancelButton(), message("disable.dialog.disable.button")), - message("disable.dialog.remove.data.box"), + message("disable.dialog.remove.data.box", providerName), false, 1, 1, Messages.getInformationIcon() ) { index: Int, checkbox: JCheckBox -> if (index == 1) { - if (checkbox.isSelected) RESULT_REMOVE_DATA_AND_DISABLE else RESULT_DISABLE + if (checkbox.isSelected) DisableSyncType.DISABLE_AND_REMOVE_DATA else DisableSyncType.DISABLE } else { - RESULT_CANCEL - } - } - - when (result) { - RESULT_DISABLE -> { - SettingsSyncSettings.getInstance().syncEnabled = false - updateStatusInfo() - SettingsSyncEventsStatistics.DISABLED_MANUALLY.log(SettingsSyncEventsStatistics.ManualDisableMethod.DISABLED_ONLY) - } - RESULT_REMOVE_DATA_AND_DISABLE -> { - disableAndRemoveData() - SettingsSyncEventsStatistics.DISABLED_MANUALLY.log( - SettingsSyncEventsStatistics.ManualDisableMethod.DISABLED_AND_REMOVED_DATA_FROM_SERVER) - } - RESULT_CANCEL -> { - SettingsSyncEventsStatistics.DISABLED_MANUALLY.log(SettingsSyncEventsStatistics.ManualDisableMethod.CANCEL) + 0 } } } private fun disableAndRemoveData() { - val modality = ModalityState.current(); + runWithModalProgressBlocking(ModalTaskOwner.component(configPanel), message("disable.remove.data.title"), TaskCancellation.cancellable()) { + val cdl = CountDownLatch(1) + SettingsSyncEvents.getInstance().fireSettingsChanged(SyncSettingsEvent.DeleteServerData { result -> + cdl.countDown() - object : Task.Modal(null, message("disable.remove.data.title"), false) { - override fun run(indicator: ProgressIndicator) { - val cdl = CountDownLatch(1) - SettingsSyncEvents.getInstance().fireSettingsChanged(SyncSettingsEvent.DeleteServerData { result -> - cdl.countDown() - - when (result) { - is DeleteServerDataResult.Error -> { - runInEdt { - showError(message("disable.remove.data.failure"), result.error) - } - } - DeleteServerDataResult.Success -> { - runInEdt(modality) { - updateStatusInfo() - } - } + when (result) { + is DeleteServerDataResult.Error -> { + val messageBuilder = StringBuilder() + messageBuilder.append(message("sync.status.failed")) + statusLabel.icon = AllIcons.General.Error + messageBuilder.append(' ').append("${message("disable.remove.data.failure")}: ${result.error}") + @Suppress("HardCodedStringLiteral") + statusLabel.text = messageBuilder.toString() } - }) - - cdl.await(1, TimeUnit.MINUTES) - } - }.queue() - } - - private fun showError(message: @Nls String, details: @Nls String) { - val messageBuilder = StringBuilder() - messageBuilder.append(message("sync.status.failed")) - statusLabel.icon = AllIcons.General.Error - messageBuilder.append(' ').append("$message: $details") - @Suppress("HardCodedStringLiteral") - statusLabel.text = messageBuilder.toString() - } - - private fun updateStatusInfo() { - if (::statusLabel.isInitialized) { - val messageBuilder = StringBuilder() - statusLabel.icon = null - if (SettingsSyncSettings.getInstance().syncEnabled) { - val statusTracker = SettingsSyncStatusTracker.getInstance() - if (statusTracker.isSyncSuccessful()) { - messageBuilder - .append(message("sync.status.enabled")) - if (statusTracker.isSynced()) { - messageBuilder - .append(". ") - .append(message("sync.status.last.sync.message", getReadableSyncTime(), getUserName())) + DeleteServerDataResult.Success -> { + syncStatusChanged() } } - else { - messageBuilder.append(message("sync.status.failed")) - statusLabel.icon = AllIcons.General.Error - messageBuilder.append(' ').append(statusTracker.getErrorMessage()) - } - } - else { - messageBuilder.append(message("sync.status.disabled")) - } - @Suppress("HardCodedStringLiteral") // The above strings are localized - statusLabel.text = messageBuilder.toString() + }) + cdl.await(1, TimeUnit.MINUTES) } } - private fun getReadableSyncTime(): String { - return DateFormatUtil.formatPrettyDateTime(SettingsSyncStatusTracker.getInstance().getLastSyncTime()).lowercase() + private fun updateSyncOptionText() { + val message = if (enableSyncOption.get() == InitSyncType.GET_FROM_SERVER) { + message("enable.dialog.get.settings.from.account.text") + } else if (enableSyncOption.get() == InitSyncType.PUSH_LOCAL) { + message("enable.dialog.sync.local.settings.text") + } else { + "" + } + syncTypeLabel.text ="
$message
" } - private fun getUserName(): String { - return RemoteCommunicatorHolder.getAuthService().getUserData().name ?: "?" + + private fun updateUserAccountsList() { + userAccountsList.clear() + val providersMap = RemoteCommunicatorHolder.getAvailableProviders().map { it.providerCode to it }.toMap() + providersMap.forEach { providerId, communicator -> + val authService = communicator.authService + val providerName = authService.providerName + authService.getAvailableUserAccounts().forEachIndexed { idx, account -> + val separatorString = if (idx == 0) + providerName + else + null + userAccountsList.add(account.toUserProviderHolder(providerName, separatorString)) + } + } + if (hasMultipleProviders.get()) { + userAccountsList.add(UserProviderHolder.addAccount) + } } + private fun login( + provider: SettingsSyncCommunicatorProvider, + syncConfigPanel: DialogPanel, + ) { + coroutineScope.launch(ModalityState.current().asContextElement()) { + try { + val userData = provider.authService.login(syncConfigPanel) + if (userData != null) { + withContext(Dispatchers.EDT) { + updateUserAccountsList() + val remoteCommunicator = RemoteCommunicatorHolder.createRemoteCommunicator(provider, userData.id) ?: return@withContext + if (checkServerState(syncPanelHolder, remoteCommunicator)) { + SettingsSyncEvents.getInstance().fireLoginStateChanged() + userDropDownLink.selectedItem = UserProviderHolder(userData.id, userData, provider.authService.providerCode, + provider.authService.providerName, null) + userDropDownLink.text + enabledStatus.set(true) + wasUsedBefore.set(true) + syncStatusChanged() + syncConfigPanel.reset() + } + } + } + else { + LOG.info("Received empty user data from login") + } + } + catch (ex: CancellationException) { + LOG.info("Login procedure was cancelled") + if (LOG.isDebugEnabled) { + LOG.info("Login procedure was cancelled", ex) + } + } + catch (ex: Throwable) { + LOG.warn("Error during login", ex) + } + syncConfigPanel.requestFocusInWindow() + } + } + + private fun SettingsSyncUserData.toUserProviderHolder(providerName: String, separatorString: String? = null) = + UserProviderHolder(id, this, providerCode, providerName, separatorString) + override fun syncStatusChanged() { - updateStatusInfo() + if (::statusLabel.isInitialized) { + if (enabledStatus.get()) { + val messageBuilder = StringBuilder() + if (SettingsSyncSettings.getInstance().syncEnabled) { + val statusTracker = SettingsSyncStatusTracker.getInstance() + if (statusTracker.isSyncSuccessful()) { + statusLabel.icon = icons.SettingsSyncIcons.StatusEnabled + if (statusTracker.isSynced()) { + messageBuilder.append(message("sync.status.last.sync.message", getReadableSyncTime())) + } else { + messageBuilder.append(message("sync.status.enabled")) + } + } + else { + messageBuilder.append(message("sync.status.failed")) + statusLabel.icon = AllIcons.General.Error + messageBuilder.append(' ').append(statusTracker.getErrorMessage()) + } + } + else { + statusLabel.icon = icons.SettingsSyncIcons.StatusNotRun + messageBuilder.append(message("sync.status.enabled")) + } + @Suppress("HardCodedStringLiteral") // The above strings are localized + statusLabel.text = messageBuilder.toString() + enableButton.text = message("config.button.disable") + } + else { + statusLabel.icon = icons.SettingsSyncIcons.StatusDisabled + statusLabel.text = message("sync.status.disabled.message") + enableButton.text = message("config.button.enable") + } + } } - override fun disposeUIResources() { - super.disposeUIResources() - SettingsSyncStatusTracker.getInstance().removeListener(this) + private fun getReadableSyncTime(): String = + DateFormatUtil.formatPrettyDateTime(SettingsSyncStatusTracker.getInstance().getLastSyncTime()) + + + private fun checkServerState(syncPanelHolder: SettingsSyncPanelHolder, communicator: SettingsSyncRemoteCommunicator) : Boolean { + communicator.setTemporary(true) + val updateResult = try { + communicator.receiveUpdates() + } + catch (ex: Exception) { + LOG.warn(ex.message) + State.CANCELLED + } + when (updateResult) { + NoFileOnServer, FileDeletedFromServer -> { + syncPanelHolder.setSyncSettings(null) + syncPanelHolder.setSyncScopeSettings(null) + enableSyncOption.set(InitSyncType.PUSH_LOCAL) + remoteSettingsExist.set(false) + return true + } + is Success -> { + syncPanelHolder.setSyncSettings(updateResult.settingsSnapshot.getState()) + syncPanelHolder.setSyncScopeSettings(SettingsSyncLocalStateHolder(updateResult.isCrossIdeSyncEnabled)) + enableSyncOption.set(InitSyncType.GET_FROM_SERVER) + remoteSettingsExist.set(true) + return true + } + is Error -> { + if (updateResult != SettingsSyncEnabler.State.CANCELLED) { + //showError(message("notification.title.update.error"), state.message) + return false + } + } + } + return false } - override fun getHelpTopic(): String = "cloud-config.plugin-dialog" + private data class UserProviderHolder( + val userId: String, + val userData: SettingsSyncUserData, + val providerCode: String, + val providerName: String, + val separatorString: String?, // separator value, set only for the first account in the list + ) { + companion object{ + val addAccount = UserProviderHolder( + "", SettingsSyncUserData("", ""), "", + "", "") + } + + override fun toString(): String { + return userData.printableName ?: userData.email ?: userData.name ?: userData.id + } + } + + private enum class InitSyncType { + PUSH_LOCAL, + GET_FROM_SERVER + } + + private sealed class DisableSyncType{ + companion object{ + const val DISABLE = 1 + const val DISABLE_AND_REMOVE_DATA = 2 + const val DONT_DISABLE = 0 + } + } + + + private class ChangeSyncTypeDialog(parent: JComponent, var option: InitSyncType) : DialogWrapper(parent, false) { + + init { + title = message("title.settings.sync") + init() + } + + override fun createCenterPanel(): JComponent { + return panel { + row { + icon(AllIcons.General.QuestionDialog).align(AlignY.TOP) + panel { + row { + text(message("enable.dialog.source.option.title")).bold() + } + row { + text(message("enable.dialog.source.option.text"), 50) + } + buttonsGroup ("", false) { + row { + radioButton(message("enable.dialog.get.settings.from.account.option"), InitSyncType.GET_FROM_SERVER) + } + row { + radioButton(message("enable.dialog.sync.local.settings.option"), InitSyncType.PUSH_LOCAL) + } + }.bind(::option) + } + } + } + } + + override fun createActions(): Array = + arrayOf(cancelAction, okAction) + } + + private class AddAccountDialog(parent: JComponent) : DialogWrapper(parent, false) { + + var providerCode: String = "" + + init { + title = message("title.settings.sync") + init() + } + + override fun createCenterPanel(): JComponent { + return panel { + row { + icon(AllIcons.General.QuestionDialog).align(AlignY.TOP) + panel { + row { + text(message("enable.sync.choose.data.provider.title")).bold() + } + buttonsGroup (message("enable.sync.choose.data.provider.text"), false) { + val availableProviders = RemoteCommunicatorHolder.getAvailableProviders() + row { + for (provider in availableProviders) { + radioButton(provider.authService.providerName, provider.providerCode) + } + } + }.bind(::providerCode) + } + } + } + } + } } -class SettingsSyncConfigurableProvider : ConfigurableProvider() { - override fun createConfigurable(): Configurable = SettingsSyncConfigurable() - - override fun canCreateConfigurable() = isSettingsSyncEnabledByKey() +class SettingsSyncConfigurableProvider(private val coroutineScope: CoroutineScope) : ConfigurableProvider() { + override fun createConfigurable(): Configurable = SettingsSyncConfigurable(coroutineScope) } \ No newline at end of file diff --git a/plugins/settings-sync/src/com/intellij/settingsSync/config/SettingsSyncEnabler.kt b/plugins/settings-sync/src/com/intellij/settingsSync/config/SettingsSyncEnabler.kt index a385fb428c39..67ba731c8ab5 100644 --- a/plugins/settings-sync/src/com/intellij/settingsSync/config/SettingsSyncEnabler.kt +++ b/plugins/settings-sync/src/com/intellij/settingsSync/config/SettingsSyncEnabler.kt @@ -1,30 +1,30 @@ package com.intellij.settingsSync.config -import com.intellij.configurationStore.saveSettings -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.components.Service -import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.Task -import com.intellij.openapi.progress.runBlockingCancellable -import com.intellij.openapi.util.NlsContexts -import com.intellij.platform.util.progress.withProgressText import com.intellij.settingsSync.* import com.intellij.settingsSync.communicator.RemoteCommunicatorHolder import com.intellij.util.EventDispatcher -import kotlinx.coroutines.* import java.util.* -internal class SettingsSyncEnabler { +class SettingsSyncEnabler { + companion object { + private val logger = logger() + } private val eventDispatcher = EventDispatcher.create(Listener::class.java) + object State { val CANCELLED = UpdateResult.Error("Cancelled") } - fun checkServerState() { + fun checkServerStateAsync() { eventDispatcher.multicaster.serverStateCheckStarted() - val communicator = RemoteCommunicatorHolder.getRemoteCommunicator() + val communicator = RemoteCommunicatorHolder.getRemoteCommunicator() ?: run { + logger.info("communicator doesn't exist, skipping check") + return + } object : Task.Modal(null, SettingsSyncBundle.message("enable.sync.check.server.data.progress"), true) { private lateinit var updateResult: UpdateResult @@ -42,6 +42,14 @@ internal class SettingsSyncEnabler { }.queue() } + fun getServerState() : UpdateResult { + val communicator = RemoteCommunicatorHolder.getRemoteCommunicator() ?: run { + logger.info("communicator doesn't exist, skipping check") + return State.CANCELLED + } + return communicator.receiveUpdates() + } + fun getSettingsFromServer(syncSettings: SettingsSyncState? = null) { eventDispatcher.multicaster.updateFromServerStarted() @@ -50,7 +58,12 @@ internal class SettingsSyncEnabler { private lateinit var updateResult: UpdateResult override fun run(indicator: ProgressIndicator) { - val result = RemoteCommunicatorHolder.getRemoteCommunicator().receiveUpdates() + val remoteCommunicator = RemoteCommunicatorHolder.getRemoteCommunicator() ?: run { + logger.info("communicator doesn't exist, cannot get settings from server") + updateResult = UpdateResult.Error("No remote communicator") + return + } + val result = remoteCommunicator.receiveUpdates() updateResult = result if (result is UpdateResult.Success) { val cloudEvent = SyncSettingsEvent.CloudChange(result.settingsSnapshot, result.serverVersionId, syncSettings) diff --git a/plugins/settings-sync/src/com/intellij/settingsSync/config/SettingsSyncOldConfigurable.kt b/plugins/settings-sync/src/com/intellij/settingsSync/config/SettingsSyncOldConfigurable.kt new file mode 100644 index 000000000000..ebaf2daab91b --- /dev/null +++ b/plugins/settings-sync/src/com/intellij/settingsSync/config/SettingsSyncOldConfigurable.kt @@ -0,0 +1,448 @@ +package com.intellij.settingsSync.config + +import com.intellij.icons.AllIcons +import com.intellij.ide.DataManager +import com.intellij.ide.plugins.InstalledPluginsState +import com.intellij.ide.plugins.PluginManagerConfigurable +import com.intellij.ide.plugins.PluginStateManager +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ApplicationNamesInfo +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.ex.ApplicationEx +import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.components.impl.stores.stateStore +import com.intellij.openapi.extensions.PluginId +import com.intellij.openapi.options.BoundConfigurable +import com.intellij.openapi.options.Configurable +import com.intellij.openapi.options.ConfigurableProvider +import com.intellij.openapi.options.ex.Settings +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.Task +import com.intellij.openapi.progress.runBlockingCancellable +import com.intellij.openapi.ui.DialogPanel +import com.intellij.openapi.ui.Messages +import com.intellij.settingsSync.* +import com.intellij.settingsSync.SettingsSyncBundle.message +import com.intellij.settingsSync.UpdateResult.* +import com.intellij.settingsSync.communicator.RemoteCommunicatorHolder +//import com.intellij.settingsSync.auth.SettingsSyncAuthService +import com.intellij.settingsSync.statistics.SettingsSyncEventsStatistics +import com.intellij.ui.components.ActionLink +import com.intellij.ui.dsl.builder.BottomGap +import com.intellij.ui.dsl.builder.Cell +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.layout.ComponentPredicate +import com.intellij.ui.layout.and +import com.intellij.ui.layout.not +import com.intellij.util.text.DateFormatUtil +import org.jetbrains.annotations.Nls +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import javax.swing.JButton +import javax.swing.JCheckBox +import javax.swing.JLabel + +internal class SettingsSyncOldConfigurable : BoundConfigurable(message("title.settings.sync")), + SettingsSyncEnabler.Listener, + SettingsSyncStatusTracker.Listener { + + private lateinit var configPanel: DialogPanel + private lateinit var enableButton: Cell + private lateinit var statusLabel: JLabel + + @Volatile + private var marketplacePluginInstalled = false + + private val syncEnabler = SettingsSyncEnabler() + private val MARKETPLACE_PLUGIN_ID = PluginId.getId("com.intellij.marketplace") + + + init { + syncEnabler.addListener(this) + SettingsSyncStatusTracker.getInstance().addListener(this) + } + + inner class LoggedInPredicate : ComponentPredicate() { + override fun addListener(listener: (Boolean) -> Unit) = + SettingsSyncEvents.getInstance().addListener( + object : SettingsSyncEventListener { + override fun loginStateChanged() { + listener(invoke()) + } + }, + disposable!!) + + override fun invoke() = RemoteCommunicatorHolder.getCurrentUserData() != null + } + + inner class EnabledPredicate : ComponentPredicate() { + override fun addListener(listener: (Boolean) -> Unit) { + SettingsSyncEvents.getInstance().addListener(object : SettingsSyncEventListener { + override fun enabledStateChanged(syncEnabled: Boolean) { + listener(invoke()) + configPanel.reset() + } + }, disposable!!) + } + + override fun invoke() = SettingsSyncSettings.getInstance().syncEnabled + + } + + inner class SyncEnablerRunning : ComponentPredicate() { + private var isRunning = false + + override fun addListener(listener: (Boolean) -> Unit) { + syncEnabler.addListener(object : SettingsSyncEnabler.Listener { + override fun serverRequestStarted() { + updateRunning(listener, true) + } + + override fun serverRequestFinished() { + updateRunning(listener, false) + } + }) + } + + private fun updateRunning(listener: (Boolean) -> Unit, isRunning: Boolean) { + this.isRunning = isRunning + listener(invoke()) + } + + override fun invoke(): Boolean = isRunning + } + + inner class AuthServiceRestartPredicate : ComponentPredicate() { + init { + marketplacePluginInstalled = InstalledPluginsState.getInstance().wasInstalled(MARKETPLACE_PLUGIN_ID) + } + + override fun addListener(listener: (Boolean) -> Unit) { + PluginStateManager.addStateListener { descriptor -> + if (descriptor.pluginId == MARKETPLACE_PLUGIN_ID) { + // InstalledPluginsState.getInstance().wasInstalled(MARKETPLACE_PLUGIN_ID) is still false at that time, + // so we just cache the value + marketplacePluginInstalled = true + listener(marketplacePluginInstalled) + } + } + } + + override fun invoke(): Boolean { + return marketplacePluginInstalled + } + } + + override fun createPanel(): DialogPanel { + val syncConfigPanel = SettingsSyncPanelFactory.createCombinedSyncSettingsPanel( + message("configurable.what.to.sync.label"), + SettingsSyncSettings.getInstance(), + SettingsSyncLocalSettings.getInstance(), + ) + val authService = RemoteCommunicatorHolder.getAuthService() + val authAvailable = true + configPanel = panel { + val isSyncEnabled = LoggedInPredicate().and(EnabledPredicate()) + if (settingsRepositoryIsEnabled()) { + row { + label(message("settings.warning.sync.cannot.be.enabled.label")).applyToComponent { + icon = AllIcons.General.Warning + } + bottomGap(BottomGap.MEDIUM) + } + } + + // authService is not available without restart + if (authAvailable) { + row { + val statusCell = label("") + statusCell + .visibleIf(LoggedInPredicate()) + .enabled(!settingsRepositoryIsEnabled()) + statusLabel = statusCell.component + updateStatusInfo() + label(message("sync.status.login.message")) + .visibleIf(LoggedInPredicate().not()) + .enabled(!settingsRepositoryIsEnabled()) + } + row { + button(message("config.button.login")) { + runBlockingCancellable { + authService?.login(configPanel) + } + }.visibleIf(LoggedInPredicate().not()) + .enabled(!settingsRepositoryIsEnabled()) + enableButton = button(message("config.button.enable")) { + syncEnabler.checkServerStateAsync() + }.visibleIf(LoggedInPredicate().and(EnabledPredicate().not())) + .enabledIf(SyncEnablerRunning().not()) + .enabled(!settingsRepositoryIsEnabled()) + + button(message("config.button.disable")) { + LoggedInPredicate().and(EnabledPredicate()) + disableSync() + }.visibleIf(isSyncEnabled) + bottomGap(BottomGap.MEDIUM) + } + } + else { + val authServiceRestartPredicate = AuthServiceRestartPredicate() + row { + label(message("sync.status.login.not.available")).gap(RightGap.SMALL) + @Suppress("DialogTitleCapitalization", "HardCodedStringLiteral") + link("JetBrains Marketplace Licensing Support") { + val settings = Settings.KEY.getData(DataManager.getInstance().getDataContext(it.source as ActionLink)) + val pluginManager = settings?.find("preferences.pluginManager") + if (pluginManager is PluginManagerConfigurable) { + settings.select(pluginManager).doWhenDone { + pluginManager.openMarketplaceTab("/organization:JetBrains Marketplace Licensing") + } + } + } + }.visibleIf(authServiceRestartPredicate.not()) + row { + label(message("sync.status.restart.required", ApplicationNamesInfo.getInstance().fullProductName)) + }.visibleIf(authServiceRestartPredicate) + row { + button(message("sync.status.restart.ide.button")) { + val app = ApplicationManager.getApplication() as ApplicationEx + app.restart(true) + } + }.visibleIf(authServiceRestartPredicate) + } + row { + comment(message("settings.sync.info.message"), 80) + .visibleIf(isSyncEnabled.not()) + } + + row { + cell(syncConfigPanel) + .visibleIf(LoggedInPredicate().and(EnabledPredicate())) + .onApply { + syncConfigPanel.apply() + + SettingsSyncEvents.getInstance().fireCategoriesChanged() + SettingsSyncEvents.getInstance().fireSettingsChanged( + SyncSettingsEvent.CrossIdeSyncStateChanged(SettingsSyncLocalSettings.getInstance().isCrossIdeSyncEnabled)) + } + .onReset(syncConfigPanel::reset) + .onIsModified(syncConfigPanel::isModified) + } + } + SettingsSyncEvents.getInstance().addListener( + object : SettingsSyncEventListener { + override fun loginStateChanged() { + if (RemoteCommunicatorHolder.getCurrentUserData() != null + && !SettingsSyncSettings.getInstance().syncEnabled) { + syncEnabler.checkServerStateAsync() + } + reset() + } + }, + disposable!! + ) + return configPanel + } + + private fun settingsRepositoryIsEnabled(): Boolean { + return !SettingsSyncSettings.getInstance().syncEnabled && + (ApplicationManager.getApplication().stateStore.storageManager).streamProvider.let { it.enabled && it.isExclusive } + } + + override fun serverStateCheckFinished(state: UpdateResult) { + when (state) { + NoFileOnServer, FileDeletedFromServer -> showEnableSyncDialog(null, null) + is Success -> showEnableSyncDialog( + state.settingsSnapshot.getState(), + SettingsSyncLocalStateHolder(state.isCrossIdeSyncEnabled), + ) + is Error -> { + if (state != SettingsSyncEnabler.State.CANCELLED) { + showError(message("notification.title.update.error"), state.message) + } + } + } + } + + override fun updateFromServerFinished(result: UpdateResult) { + when (result) { + is Success -> { + reset() + SettingsSyncSettings.getInstance().syncEnabled = true + } + NoFileOnServer, FileDeletedFromServer -> { + showError(message("notification.title.update.error"), message("notification.title.update.no.such.file")) + } + is Error -> { + showError(message("notification.title.update.error"), result.message) + } + } + updateStatusInfo() + } + + private fun showEnableSyncDialog(remoteSettings: SettingsSyncState?, remoteSyncScopeSettings: SettingsSyncLocalStateHolder?) { + val dialog = EnableSettingsSyncDialog(configPanel, remoteSettings, remoteSyncScopeSettings) + + dialog.show() + + val dialogResult = dialog.getResult() + + if (dialogResult != null) { + when (dialogResult) { + EnableSettingsSyncDialog.Result.GET_FROM_SERVER -> { + syncEnabler.getSettingsFromServer(dialog.syncSettings) + + SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.GET_FROM_SERVER) + } + + EnableSettingsSyncDialog.Result.PUSH_LOCAL -> { + SettingsSyncSettings.getInstance().syncEnabled = true + + syncEnabler.pushSettingsToServer() + + if (remoteSettings != null) { + SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.PUSH_LOCAL) + } + else { + SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.PUSH_LOCAL_WAS_ONLY_WAY) + } + } + } + } + else { + SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.CANCELED) + } + + reset() + configPanel.reset() + } + + companion object DisableResult { + const val RESULT_CANCEL = 0 + const val RESULT_REMOVE_DATA_AND_DISABLE = 1 + const val RESULT_DISABLE = 2 + } + + private fun disableSync() { + @Suppress("DialogTitleCapitalization") + val result = Messages.showCheckboxMessageDialog( // TODO: Use AlertMessage instead + message("disable.dialog.text"), + message("disable.dialog.title"), + arrayOf(Messages.getCancelButton(), message("disable.dialog.disable.button")), + message("disable.dialog.remove.data.box"), + false, + 1, + 1, + Messages.getInformationIcon() + ) { index: Int, checkbox: JCheckBox -> + if (index == 1) { + if (checkbox.isSelected) RESULT_REMOVE_DATA_AND_DISABLE else RESULT_DISABLE + } + else { + RESULT_CANCEL + } + } + + when (result) { + RESULT_DISABLE -> { + SettingsSyncSettings.getInstance().syncEnabled = false + updateStatusInfo() + SettingsSyncEventsStatistics.DISABLED_MANUALLY.log(SettingsSyncEventsStatistics.ManualDisableMethod.DISABLED_ONLY) + } + RESULT_REMOVE_DATA_AND_DISABLE -> { + disableAndRemoveData() + SettingsSyncEventsStatistics.DISABLED_MANUALLY.log( + SettingsSyncEventsStatistics.ManualDisableMethod.DISABLED_AND_REMOVED_DATA_FROM_SERVER) + } + RESULT_CANCEL -> { + SettingsSyncEventsStatistics.DISABLED_MANUALLY.log(SettingsSyncEventsStatistics.ManualDisableMethod.CANCEL) + } + } + } + + private fun disableAndRemoveData() { + val modality = ModalityState.current(); + + object : Task.Modal(null, message("disable.remove.data.title"), false) { + override fun run(indicator: ProgressIndicator) { + val cdl = CountDownLatch(1) + SettingsSyncEvents.getInstance().fireSettingsChanged(SyncSettingsEvent.DeleteServerData { result -> + cdl.countDown() + + when (result) { + is DeleteServerDataResult.Error -> { + runInEdt { + showError(message("disable.remove.data.failure"), result.error) + } + } + DeleteServerDataResult.Success -> { + runInEdt(modality) { + updateStatusInfo() + } + } + } + }) + + cdl.await(1, TimeUnit.MINUTES) + } + }.queue() + } + + private fun showError(message: @Nls String, details: @Nls String) { + val messageBuilder = StringBuilder() + messageBuilder.append(message("sync.status.failed")) + statusLabel.icon = AllIcons.General.Error + messageBuilder.append(' ').append("$message: $details") + @Suppress("HardCodedStringLiteral") + statusLabel.text = messageBuilder.toString() + } + + private fun updateStatusInfo() { + if (::statusLabel.isInitialized) { + val messageBuilder = StringBuilder() + statusLabel.icon = null + if (SettingsSyncSettings.getInstance().syncEnabled) { + val statusTracker = SettingsSyncStatusTracker.getInstance() + if (statusTracker.isSyncSuccessful()) { + messageBuilder + .append(message("sync.status.enabled")) + if (statusTracker.isSynced()) { + messageBuilder + .append(". ") + .append(message("sync.status.last.sync.message", getReadableSyncTime())) + } + } + else { + messageBuilder.append(message("sync.status.failed")) + statusLabel.icon = AllIcons.General.Error + messageBuilder.append(' ').append(statusTracker.getErrorMessage()) + } + } + else { + messageBuilder.append(message("sync.status.disabled")) + } + @Suppress("HardCodedStringLiteral") // The above strings are localized + statusLabel.text = messageBuilder.toString() + } + } + + private fun getReadableSyncTime(): String { + return DateFormatUtil.formatPrettyDateTime(SettingsSyncStatusTracker.getInstance().getLastSyncTime()).lowercase() + } + + private fun getUserName(): String { + return RemoteCommunicatorHolder.getCurrentUserData()?.name ?: "?" + } + + override fun syncStatusChanged() { + updateStatusInfo() + } + + override fun disposeUIResources() { + super.disposeUIResources() + SettingsSyncStatusTracker.getInstance().removeListener(this) + } + + override fun getHelpTopic(): String = "cloud-config.plugin-dialog" +} \ No newline at end of file diff --git a/plugins/settings-sync/src/com/intellij/settingsSync/config/SettingsSyncPanelFactory.kt b/plugins/settings-sync/src/com/intellij/settingsSync/config/SettingsSyncPanelFactory.kt index dbff996bcdea..22920220732c 100644 --- a/plugins/settings-sync/src/com/intellij/settingsSync/config/SettingsSyncPanelFactory.kt +++ b/plugins/settings-sync/src/com/intellij/settingsSync/config/SettingsSyncPanelFactory.kt @@ -1,6 +1,8 @@ package com.intellij.settingsSync.config import com.intellij.openapi.application.ApplicationNamesInfo +import com.intellij.openapi.components.SettingsCategory +import com.intellij.openapi.observable.properties.AtomicBooleanProperty import com.intellij.openapi.ui.DialogPanel import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.settingsSync.* @@ -30,17 +32,41 @@ internal object SettingsSyncPanelFactory { syncSettings: SettingsSyncState, syncScopeSettings: SettingsSyncLocalState, ): DialogPanel { - val categoriesPanel = createSyncCategoriesPanel(syncLabel, syncSettings) - val syncScopePanel = createSyncScopePanel(syncScopeSettings) + return SettingsSyncPanelHolder().createCombinedSyncSettingsPanel(syncLabel, syncSettings, syncScopeSettings) + } +} +internal class SettingsSyncPanelHolder() { + private lateinit var panel : DialogPanel + private var isCrossIdeSyncEnabled = false - return panel { + fun setSyncSettings(syncSettings: SettingsSyncState?) { + val notNullState = syncSettings ?: SettingsSyncStateHolder() + SyncCategoryHolder.updateState(notNullState) + } + + fun setSyncScopeSettings(settings: SettingsSyncLocalState?) { + isCrossIdeSyncEnabled = settings?.isCrossIdeSyncEnabled ?: false + } + + fun createCombinedSyncSettingsPanel( + syncLabel: @Nls String, + syncSettings: SettingsSyncState?, + syncScopeSettings: SettingsSyncLocalState?, + ): DialogPanel { + setSyncSettings(syncSettings) + setSyncScopeSettings(syncScopeSettings) + val categoriesPanel = createSyncCategoriesPanel(syncLabel) + val syncScopePanel = createSyncScopePanel() + + panel = panel { row { cell(categoriesPanel) .onApply(categoriesPanel::apply) .onReset(categoriesPanel::reset) - .onIsModified(categoriesPanel::isModified) + .onIsModified { + categoriesPanel.isModified() + } } - row { cell(syncScopePanel) .onApply(syncScopePanel::apply) @@ -48,14 +74,23 @@ internal object SettingsSyncPanelFactory { .onIsModified(syncScopePanel::isModified) } onApply { - SettingsSyncLocalSettings.getInstance().applyFromState(syncScopeSettings) - SettingsSyncSettings.getInstance().applyFromState(syncSettings) + // do nothing, handled by descendants } + onIsModified { + categoriesPanel.isModified() || syncScopePanel.isModified() + } + } + return panel } - private fun createSyncScopePanel(state: SettingsSyncLocalState): DialogPanel { + private fun createSyncScopePanel(): DialogPanel { return panel { + onApply { + SettingsSyncLocalSettings.getInstance().isCrossIdeSyncEnabled = isCrossIdeSyncEnabled + SettingsSyncEvents.getInstance().fireSettingsChanged( + SyncSettingsEvent.CrossIdeSyncStateChanged(SettingsSyncLocalSettings.getInstance().isCrossIdeSyncEnabled)) + } row { topGap(TopGap.MEDIUM) label(message("settings.cross.product.sync")) @@ -68,27 +103,42 @@ internal object SettingsSyncPanelFactory { row { radioButton(message("settings.cross.product.sync.choice.all.products"), true) } - }.bind(state::isCrossIdeSyncEnabled) + }.bind(::isCrossIdeSyncEnabled) } } - private fun createSyncCategoriesPanel(syncLabel: @Nls String, state: SettingsSyncState): DialogPanel { + private fun createSyncCategoriesPanel(syncLabel: @Nls String): DialogPanel { return panel { + onApply { + SettingsSyncSettings.getInstance().updateCategories( + SyncCategoryHolder.disabledCategories, + SyncCategoryHolder.disabledSubcategories + ) + SettingsSyncEvents.getInstance().fireCategoriesChanged() + } row { label(syncLabel) } - val categoryHolders = SyncCategoryHolder.createAllForState(state) - for (holder in categoryHolders) { + for (holder in SyncCategoryHolder.allHolders) { indent { row { if (holder.secondaryGroup == null) { - checkBox( + val checkBox = checkBox( holder.name ) + checkBox .bindSelected(holder::isSynchronized) - .onReset { holder.reset() } - .onApply { holder.apply() } - .onIsModified { holder.isModified() } + .onReset { + holder.reset() + checkBox.component.isSelected = holder.isSynchronized + } + .onApply { + holder.apply() + } + .onIsModified { + holder.isModified() + } + .enabled(isModifiable(holder)) comment(holder.description) } else { @@ -109,8 +159,12 @@ internal object SettingsSyncPanelFactory { topCheckBox.state = getGroupState(holder) holder.isSynchronized = topCheckBox.state != State.NOT_SELECTED } - cell(subcategoryLink) + val subcategoryLinkCell = cell(subcategoryLink) + subcategoryLinkCell .visible(holder.secondaryGroup!!.getDescriptors().size > 1 || !holder.secondaryGroup!!.isComplete()) + .onReset { + subcategoryLinkCell.visible(holder.secondaryGroup!!.getDescriptors().size > 1 || !holder.secondaryGroup!!.isComplete()) + } topCheckBox.addActionListener { holder.isSynchronized = topCheckBox.state != State.NOT_SELECTED holder.secondaryGroup!!.getDescriptors().forEach { @@ -119,7 +173,6 @@ internal object SettingsSyncPanelFactory { subcategoryLink.isEnabled = holder.secondaryGroup!!.isComplete() || holder.isSynchronized } } - } } } diff --git a/plugins/settings-sync/src/com/intellij/settingsSync/config/SettingsSyncStatusAction.kt b/plugins/settings-sync/src/com/intellij/settingsSync/config/SettingsSyncStatusAction.kt index 633ae2143ecf..a842370bc74d 100644 --- a/plugins/settings-sync/src/com/intellij/settingsSync/config/SettingsSyncStatusAction.kt +++ b/plugins/settings-sync/src/com/intellij/settingsSync/config/SettingsSyncStatusAction.kt @@ -21,7 +21,7 @@ private enum class SyncStatus {ON, OFF, FAILED} private fun getStatus() : SyncStatus { if (SettingsSyncSettings.getInstance().syncEnabled && - RemoteCommunicatorHolder.getAuthService().isLoggedIn()) { + RemoteCommunicatorHolder.getCurrentUserData() != null) { return if (SettingsSyncStatusTracker.getInstance().isSyncSuccessful()) SyncStatus.ON else SyncStatus.FAILED } diff --git a/plugins/settings-sync/src/com/intellij/settingsSync/config/SyncCategoryHolder.kt b/plugins/settings-sync/src/com/intellij/settingsSync/config/SyncCategoryHolder.kt index ed0cb7553b59..b828e58d97da 100644 --- a/plugins/settings-sync/src/com/intellij/settingsSync/config/SyncCategoryHolder.kt +++ b/plugins/settings-sync/src/com/intellij/settingsSync/config/SyncCategoryHolder.kt @@ -7,11 +7,11 @@ import com.intellij.settingsSync.SettingsSyncBundle.message import org.jetbrains.annotations.Nls import java.util.* -internal class SyncCategoryHolder( - val descriptor: Category, - private val state: SettingsSyncState -) { - var isSynchronized: Boolean = state.isCategoryEnabled(descriptor.category) +internal class SyncCategoryHolder(val descriptor: Category) { + + private var state: SettingsSyncState? = null + + var isSynchronized: Boolean = state?.isCategoryEnabled(descriptor.category) ?: false val name: @Nls String get() = descriptor.name @@ -24,10 +24,10 @@ internal class SyncCategoryHolder( fun reset() { with(descriptor) { - isSynchronized = state.isCategoryEnabled(category) + isSynchronized = state?.isCategoryEnabled(category) ?: false if (secondaryGroup != null) { secondaryGroup.getDescriptors().forEach { - it.isSelected = isSynchronized && state.isSubcategoryEnabled(category, it.id) + it.isSelected = isSynchronized && state?.isSubcategoryEnabled(category, it.id) ?: false } } } @@ -38,38 +38,68 @@ internal class SyncCategoryHolder( if (secondaryGroup != null) { secondaryGroup.getDescriptors().forEach { // !isSynchronized not store disabled states individually - state.setSubcategoryEnabled(category, it.id, !isSynchronized || it.isSelected) + state?.setSubcategoryEnabled(category, it.id, !isSynchronized || it.isSelected) } } - state.setCategoryEnabled(category, isSynchronized) + state?.setCategoryEnabled(category, isSynchronized) } } fun isModified(): Boolean { with(descriptor) { - if (isSynchronized != state.isCategoryEnabled(category)) return true + if (isSynchronized != state?.isCategoryEnabled(category)) return true if (secondaryGroup != null && isSynchronized) { secondaryGroup.getDescriptors().forEach { - if (it.isSelected != state.isSubcategoryEnabled(category, it.id)) return true + if (it.isSelected != state?.isSubcategoryEnabled(category, it.id)) return true } } return false } } + override fun toString(): String { + return "SyncCategoryHolder(name='$name', isSynchronized=$isSynchronized, isModified=${isModified()})" + } + + companion object { - fun createAllForState(state: SettingsSyncState): List { - val retval = arrayListOf() + val allHolders: List = arrayListOf().apply { for (descriptor in Category.DESCRIPTORS) { - retval.add(SyncCategoryHolder(descriptor, state)) + add(SyncCategoryHolder(descriptor)) } - return retval } + + fun updateState(state: SettingsSyncState) { + allHolders.forEach { + it.state = state + } + } + + val disabledCategories: List + get() = arrayListOf().apply { + for (holder in allHolders) { + if (!holder.isSynchronized) { + add(holder.descriptor.category) + } + } + } + + val disabledSubcategories: Map> + get() = hashMapOf>().apply { + for (holder in allHolders) { + val descriptors = holder.secondaryGroup?.getDescriptors() ?: continue + for (descriptor in descriptors) { + if (!descriptor.isSelected) { + computeIfAbsent(holder.descriptor.category) { arrayListOf() }.add(descriptor.id) + } + } + } + } } internal class Category( val category: SettingsCategory, - val secondaryGroup: SyncSubcategoryGroup? = null + val secondaryGroup: SyncSubcategoryGroup? = null, ) { val name: @Nls String diff --git a/plugins/settings-sync/src/com/intellij/settingsSync/performanceTesting/EnableSettingsSyncCommand.kt b/plugins/settings-sync/src/com/intellij/settingsSync/performanceTesting/EnableSettingsSyncCommand.kt index f211dc1e9849..067938615b3e 100644 --- a/plugins/settings-sync/src/com/intellij/settingsSync/performanceTesting/EnableSettingsSyncCommand.kt +++ b/plugins/settings-sync/src/com/intellij/settingsSync/performanceTesting/EnableSettingsSyncCommand.kt @@ -6,7 +6,6 @@ import com.intellij.openapi.ui.playback.commands.PlaybackCommandCoroutineAdapter import com.intellij.settingsSync.* import com.intellij.settingsSync.communicator.RemoteCommunicatorHolder import com.intellij.settingsSync.config.SettingsSyncEnabler -import com.jetbrains.performancePlugin.commands.Waiter import kotlinx.coroutines.* import org.jetbrains.annotations.NonNls import java.util.concurrent.TimeUnit @@ -64,7 +63,7 @@ class EnableSettingsSyncCommand(text: @NonNls String, line: Int) : PlaybackComma serverRespondedOnCheck.complete(true) } }) - settingsSyncEnabler.checkServerState() + settingsSyncEnabler.checkServerStateAsync() serverRespondedOnCheck.await() var startTime = System.currentTimeMillis() @@ -81,11 +80,11 @@ class EnableSettingsSyncCommand(text: @NonNls String, line: Int) : PlaybackComma } //there is no event that cross-ide sync was enabled, so we need to check that file appears and wait a bit :( startTime = System.currentTimeMillis() - val remoteCommunicator = RemoteCommunicatorHolder.getRemoteCommunicator() - while (remoteCommunicator.isFileExists(CROSS_IDE_SYNC_MARKER_FILE) != isCrossIdeSync) { + while (RemoteCommunicatorHolder.getRemoteCommunicator() != null && + RemoteCommunicatorHolder.getRemoteCommunicator()?.isFileExists(CROSS_IDE_SYNC_MARKER_FILE) != isCrossIdeSync) { delay(500) if (System.currentTimeMillis() - startTime > TimeUnit.SECONDS.toMillis(5)) { - val fileExists = remoteCommunicator.isFileExists(CROSS_IDE_SYNC_MARKER_FILE) + val fileExists = RemoteCommunicatorHolder.getRemoteCommunicator()?.isFileExists(CROSS_IDE_SYNC_MARKER_FILE) throw Exception("Cross-IDE sync marker file was not updated in 5 seconds. File exists=$fileExists, expected=$isCrossIdeSync") } } diff --git a/plugins/settings-sync/src/icons/SettingsSyncIcons.java b/plugins/settings-sync/src/icons/SettingsSyncIcons.java index 8d64782ea3fc..9ece3d4709dc 100644 --- a/plugins/settings-sync/src/icons/SettingsSyncIcons.java +++ b/plugins/settings-sync/src/icons/SettingsSyncIcons.java @@ -10,6 +10,9 @@ import javax.swing.*; * DO NOT EDIT IT BY HAND, run "Generate icon classes" configuration instead */ public final class SettingsSyncIcons { + private static @NotNull Icon load(@NotNull String path, int cacheKey, int flags) { + return IconManager.getInstance().loadRasterizedIcon(path, SettingsSyncIcons.class.getClassLoader(), cacheKey, flags); + } private static @NotNull Icon load(@NotNull String expUIPath, @NotNull String path, int cacheKey, int flags) { return IconManager.getInstance().loadRasterizedIcon(path, expUIPath, SettingsSyncIcons.class.getClassLoader(), cacheKey, flags); } @@ -18,4 +21,5 @@ public final class SettingsSyncIcons { /** 16x16 */ public static final @NotNull Icon RemoteChanges = load("icons/expui/remoteChanges.svg", "icons/remoteChanges.svg", 287889654, 0); /** 16x16 */ public static final @NotNull Icon StatusDisabled = load("icons/expui/statusDisabled.svg", "icons/statusDisabled.svg", -1854758088, 2); /** 16x16 */ public static final @NotNull Icon StatusEnabled = load("icons/expui/statusEnabled.svg", "icons/statusEnabled.svg", 1900690067, 0); + /** 16x16 */ public static final @NotNull Icon StatusNotRun = load("icons/statusNotRun.svg", 1430629267, 2); } diff --git a/plugins/settings-sync/tests/com/intellij/settingsSync/GitSettingsLogTest.kt b/plugins/settings-sync/tests/com/intellij/settingsSync/GitSettingsLogTest.kt index 6d3b02bad6b1..f9613fa27bca 100644 --- a/plugins/settings-sync/tests/com/intellij/settingsSync/GitSettingsLogTest.kt +++ b/plugins/settings-sync/tests/com/intellij/settingsSync/GitSettingsLogTest.kt @@ -471,14 +471,14 @@ internal class GitSettingsLogTest { fun `use empty email if JBA doesn't provide one`() { val jbaName = "JBA Name 2" - userData = SettingsSyncUserData(jbaName, null) + userData = SettingsSyncUserData(jbaName, "dummy", jbaName, null) checkUsernameEmail(jbaName, "") } @Test @TestFor(issues = ["EA-844607"]) fun `use empty name if JBA doesn't provide one`() { - userData = SettingsSyncUserData(null, null) + userData = SettingsSyncUserData("empty", "dummy", null, null) checkUsernameEmail("", "") } diff --git a/plugins/settings-sync/tests/com/intellij/settingsSync/MockRemoteCommunicator.kt b/plugins/settings-sync/tests/com/intellij/settingsSync/MockRemoteCommunicator.kt index 9fbd1670d747..c38bd4323b97 100644 --- a/plugins/settings-sync/tests/com/intellij/settingsSync/MockRemoteCommunicator.kt +++ b/plugins/settings-sync/tests/com/intellij/settingsSync/MockRemoteCommunicator.kt @@ -5,7 +5,10 @@ import com.intellij.openapi.util.io.FileUtil import com.intellij.settingsSync.auth.SettingsSyncAuthService import com.intellij.settingsSync.communicator.SettingsSyncCommunicatorProvider import com.intellij.settingsSync.communicator.SettingsSyncUserData +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred import org.junit.Assert +import java.awt.Component import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.IOException @@ -14,9 +17,10 @@ import java.time.Instant import java.util.* import java.util.concurrent.CountDownLatch import java.util.concurrent.atomic.AtomicInteger +import javax.swing.Icon import kotlin.isInitialized -internal class MockRemoteCommunicator : AbstractServerCommunicator() { +internal class MockRemoteCommunicator(override val userId: String) : AbstractServerCommunicator() { private val filesAndVersions = mutableMapOf() private val versionIdStorage = mutableMapOf() private val LOG = logger() @@ -43,11 +47,11 @@ internal class MockRemoteCommunicator : AbstractServerCommunicator() { return e.message ?: "unknown error" } - override fun readFileInternal(snapshotFilePath: String): Pair { + override fun readFileInternal(filePath: String): Pair { checkConnected() - val version = filesAndVersions[snapshotFilePath] ?: throw IOException("file $snapshotFilePath is not found") - versionIdStorage.put(snapshotFilePath, version.versionId) - LOG.warn("Put version '${version.versionId}' for file $snapshotFilePath (after read)") + val version = filesAndVersions[filePath] ?: throw IOException("file $filePath is not found") + versionIdStorage.put(filePath, version.versionId) + LOG.warn("Put version '${version.versionId}' for file $filePath (after read)") return Pair(ByteArrayInputStream(version.content), version.versionId) } @@ -150,7 +154,7 @@ internal class MockCommunicatorProvider ( override val providerCode: String get() = "MOCK" - override fun createCommunicator(): SettingsSyncRemoteCommunicator? = remoteCommunicator + override fun createCommunicator(userId: String): SettingsSyncRemoteCommunicator? = remoteCommunicator } internal class MockAuthService ( @@ -158,21 +162,21 @@ internal class MockAuthService ( ): SettingsSyncAuthService { override val providerCode: String get() = "MOCK" + override val providerName: String + get() = TODO("Not yet implemented") + override val icon: Icon? + get() = TODO("Not yet implemented") - override fun login() { - // do nothing + override suspend fun login(parentComponent: Component?) : SettingsSyncUserData? { + return null } - override fun isLoggedIn(): Boolean { - return true - } - - override fun getUserData(): SettingsSyncUserData { + override fun getUserData(userId: String): SettingsSyncUserData { return userData } - override fun isLoginAvailable(): Boolean { - return true + override fun getAvailableUserAccounts(): List { + TODO("Not yet implemented") } } \ No newline at end of file diff --git a/plugins/settings-sync/tests/com/intellij/settingsSync/SettingsSyncFlowTest.kt b/plugins/settings-sync/tests/com/intellij/settingsSync/SettingsSyncFlowTest.kt index 7a03670045eb..772c2804a7bf 100644 --- a/plugins/settings-sync/tests/com/intellij/settingsSync/SettingsSyncFlowTest.kt +++ b/plugins/settings-sync/tests/com/intellij/settingsSync/SettingsSyncFlowTest.kt @@ -73,7 +73,7 @@ internal class SettingsSyncFlowTest : SettingsSyncTestBase() { // emulate first session with initialization val fileName = "options/laf.xml" val file = configDir.resolve(fileName).write("LaF Initial") - val log = GitSettingsLog(settingsSyncStorage, configDir, disposable, { SettingsSyncUserData.EMPTY }, + val log = GitSettingsLog(settingsSyncStorage, configDir, disposable, { SettingsSyncUserData("empty", "dummy") }, initialSnapshotProvider = { MockSettingsSyncIdeMediator.getAllFilesFromSettingsAsSnapshot(configDir) }) log.initialize() log.logExistingSettings() diff --git a/plugins/settings-sync/tests/com/intellij/settingsSync/SettingsSyncRealIdeTestBase.kt b/plugins/settings-sync/tests/com/intellij/settingsSync/SettingsSyncRealIdeTestBase.kt index 3ad967b22a53..7992d06780cc 100644 --- a/plugins/settings-sync/tests/com/intellij/settingsSync/SettingsSyncRealIdeTestBase.kt +++ b/plugins/settings-sync/tests/com/intellij/settingsSync/SettingsSyncRealIdeTestBase.kt @@ -167,7 +167,7 @@ internal abstract class SettingsSyncRealIdeTestBase : SettingsSyncTestBase() { tempDir.resolve("storage").toPath(), tempDir.resolve("config").toPath(), parentDisposable, - { SettingsSyncUserData.EMPTY }, + { SettingsSyncUserData("empty", "dummy") }, initialSnapshotProvider = { SettingsSnapshot( SettingsSnapshot.MetaInfo(Instant.now(), null, true), diff --git a/plugins/settings-sync/tests/com/intellij/settingsSync/SettingsSyncTestBase.kt b/plugins/settings-sync/tests/com/intellij/settingsSync/SettingsSyncTestBase.kt index 3bd7f701b13e..2c7491e4d145 100644 --- a/plugins/settings-sync/tests/com/intellij/settingsSync/SettingsSyncTestBase.kt +++ b/plugins/settings-sync/tests/com/intellij/settingsSync/SettingsSyncTestBase.kt @@ -55,7 +55,7 @@ internal abstract class SettingsSyncTestBase { TODO("Implement with real server via TestRemoteCommunicator()") } else { - MockRemoteCommunicator().apply {this.isConnected = true } + MockRemoteCommunicator("mockUser").apply {this.isConnected = true } } val providerEP = SettingsSyncCommunicatorProvider.PROVIDER_EP.point if (providerEP.extensions.size > 0) {