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) {