From 69bace9d1b94b042ddf7cd0237d7eccf8a504fe5 Mon Sep 17 00:00:00 2001 From: Nikita Zaytsev Date: Tue, 14 Oct 2025 19:46:31 +0000 Subject: [PATCH] [rdct] IJPL-199766: Implement file-based settings sync Some settings are only used in the monolith and on the backend. In this case, PersistentStateComponents aren't loaded on the frontend so the RdSettingsController can't be used to sync these settings. They can only be synced between the frontend and backend as raw files. GitOrigin-RevId: 226200051312e64123941aeacee626883b6c8b03 --- .../src/CompoundOperationListener.kt | 30 +++++++++++++++ .../src/FileBasedStorage.kt | 3 +- .../src/ModuleStoreImpl.kt | 1 + .../src/StateStorageManagerImpl.kt | 9 +++++ .../src/XmlElementStorage.kt | 38 +++++++++++-------- platform/platform-impl/api-dump.txt | 5 +++ .../FileBasedSettingsSyncListProvider.kt | 22 +++++++++++ .../configurationStore/OperationListener.kt | 29 ++++++++++++++ .../configurationStore/StateStorageManager.kt | 7 ++++ 9 files changed, 128 insertions(+), 16 deletions(-) create mode 100644 platform/configuration-store-impl/src/CompoundOperationListener.kt create mode 100644 platform/platform-impl/src/com/intellij/ide/settings/fileBasedSync/FileBasedSettingsSyncListProvider.kt create mode 100644 platform/projectModel-api/src/com/intellij/configurationStore/OperationListener.kt diff --git a/platform/configuration-store-impl/src/CompoundOperationListener.kt b/platform/configuration-store-impl/src/CompoundOperationListener.kt new file mode 100644 index 000000000000..fee05417b457 --- /dev/null +++ b/platform/configuration-store-impl/src/CompoundOperationListener.kt @@ -0,0 +1,30 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.configurationStore + +import java.util.concurrent.CopyOnWriteArrayList + +/** + * Contains multiple listeners and delegates method calls to them + */ +internal class CompoundOperationListener : OperationListener { + + private val listeners = CopyOnWriteArrayList() + + override fun onWrite(fileSpec: String, content: ByteArray) { + listeners.forEach { it.onWrite(fileSpec, content) } + } + + override fun onDelete(fileSpec: String) { + listeners.forEach { it.onDelete(fileSpec) } + } + + /** + * Adds a listener to delegate method calls to + * + * @param listener the listener + */ + fun addOperationListener(listener: OperationListener) { + listeners.add(listener) + } + +} diff --git a/platform/configuration-store-impl/src/FileBasedStorage.kt b/platform/configuration-store-impl/src/FileBasedStorage.kt index e8c744843c9d..d32f24334382 100644 --- a/platform/configuration-store-impl/src/FileBasedStorage.kt +++ b/platform/configuration-store-impl/src/FileBasedStorage.kt @@ -46,7 +46,8 @@ abstract class FileBasedStorage internal constructor( pathMacroManager: PathMacroSubstitutor? = null, roamingType: RoamingType, provider: StreamProvider? = null, -) : XmlElementStorage(fileSpec = fileSpec, rootElementName = rootElementName, pathMacroSubstitutor = pathMacroManager, storageRoamingType = roamingType, provider = provider) { + listener: OperationListener? = null +) : XmlElementStorage(fileSpec = fileSpec, rootElementName = rootElementName, pathMacroSubstitutor = pathMacroManager, storageRoamingType = roamingType, provider = provider, listener = listener) { @Volatile private var cachedVirtualFile: VirtualFile? = null private var lineSeparator: LineSeparator? = null diff --git a/platform/configuration-store-impl/src/ModuleStoreImpl.kt b/platform/configuration-store-impl/src/ModuleStoreImpl.kt index eabce59e0b28..230574450a0b 100644 --- a/platform/configuration-store-impl/src/ModuleStoreImpl.kt +++ b/platform/configuration-store-impl/src/ModuleStoreImpl.kt @@ -187,6 +187,7 @@ private class ModuleStateStorageManager(macroSubstitutor: TrackingPathMacroSubst roamingType = roamingType, pathMacroManager = macroSubstitutor, provider = provider, + listener = null, controller = null, ) } diff --git a/platform/configuration-store-impl/src/StateStorageManagerImpl.kt b/platform/configuration-store-impl/src/StateStorageManagerImpl.kt index d207eb125f8d..3c636b1ab96a 100644 --- a/platform/configuration-store-impl/src/StateStorageManagerImpl.kt +++ b/platform/configuration-store-impl/src/StateStorageManagerImpl.kt @@ -55,6 +55,12 @@ open class StateStorageManagerImpl( compoundStreamProvider.removeStreamProvider(aClass) } + private var compoundOperationListener = CompoundOperationListener() + + override fun addOperationListener(listener: OperationListener) { + compoundOperationListener.addOperationListener(listener) + } + // access under storageLock private var isUseVfsListener = when (componentManager) { null -> ThreeState.NO @@ -268,6 +274,7 @@ open class StateStorageManagerImpl( roamingType = roamingType, pathMacroManager = macroSubstitutor.takeIf { usePathMacroManager }, provider = compoundStreamProvider, + listener = compoundOperationListener, controller = controller, ) } @@ -280,6 +287,7 @@ open class StateStorageManagerImpl( roamingType: RoamingType, pathMacroManager: PathMacroSubstitutor?, provider: StreamProvider?, + listener: OperationListener?, override val controller: SettingsController?, ) : FileBasedStorage( file = file, @@ -288,6 +296,7 @@ open class StateStorageManagerImpl( pathMacroManager = pathMacroManager, roamingType = roamingType, provider = provider, + listener = listener, ), StorageVirtualFileTracker.TrackedStorage { override val isUseXmlProlog: Boolean get() = rootElementName != null && storageManager.isUseXmlProlog && !isSpecialStorage(fileSpec) diff --git a/platform/configuration-store-impl/src/XmlElementStorage.kt b/platform/configuration-store-impl/src/XmlElementStorage.kt index 606499839f53..bb814aa3bcd5 100644 --- a/platform/configuration-store-impl/src/XmlElementStorage.kt +++ b/platform/configuration-store-impl/src/XmlElementStorage.kt @@ -37,7 +37,8 @@ abstract class XmlElementStorage protected constructor( @JvmField protected val rootElementName: String?, private val pathMacroSubstitutor: PathMacroSubstitutor? = null, storageRoamingType: RoamingType, - private val provider: StreamProvider? = null + private val provider: StreamProvider? = null, + private val listener: OperationListener? = null ) : StateStorageBase() { final override val saveStorageDataOnReload: Boolean get() = provider == null || provider.saveStorageDataOnReload @@ -257,34 +258,30 @@ abstract class XmlElementStorage protected constructor( override suspend fun save(events: MutableList?) { var isSavedLocally = false val provider = storage.provider + val listener = storage.listener if (elements == null) { - if (provider == null || !provider.delete(storage.fileSpec, storage.roamingType)) { + if (provider != null && provider.delete(storage.fileSpec, storage.roamingType)) { + listener?.onDelete(storage.fileSpec) + } + else { isSavedLocally = true - if (writer == null) { - remove(events) - } - else { - saveLocally(dataWriter = writer, events = events) - } + removeOrSaveLocally(events) } } else if (provider != null && provider.isApplicable(storage.fileSpec, storage.roamingType)) { // we should use standard line-separator (\n) - stream provider can share file content on any OS + val content = writer!!.toBufferExposingByteArray(LineSeparator.LF).toByteArray() provider.write( fileSpec = storage.fileSpec, - content = writer!!.toBufferExposingByteArray(LineSeparator.LF).toByteArray(), + content = content, roamingType = storage.roamingType, ) + listener?.onWrite(storage.fileSpec, content) } else { isSavedLocally = true - if (writer == null) { - remove(events) - } - else { - saveLocally(dataWriter = writer, events = events) - } + removeOrSaveLocally(events) } if (!isSavedLocally) { @@ -293,6 +290,17 @@ abstract class XmlElementStorage protected constructor( storage.setStates(originalStates, stateMap) } + + private fun removeOrSaveLocally(events: MutableList?) { + if (writer == null) { + remove(events) + storage.listener?.onDelete(storage.fileSpec) + } + else { + saveLocally(dataWriter = writer, events = events) + storage.listener?.onWrite(storage.fileSpec, writer.toBufferExposingByteArray(LineSeparator.LF).toByteArray()) + } + } } override fun setSerializedState(componentName: String, element: Element?) { diff --git a/platform/platform-impl/api-dump.txt b/platform/platform-impl/api-dump.txt index 2f2d84ed9123..e5f803e82118 100644 --- a/platform/platform-impl/api-dump.txt +++ b/platform/platform-impl/api-dump.txt @@ -309,6 +309,11 @@ bf:com.intellij.ide.plugins.marketplace.IdeCompatibleUpdate$$serializer - f:deserialize(kotlinx.serialization.encoding.Decoder):com.intellij.ide.plugins.marketplace.IdeCompatibleUpdate - f:getDescriptor():kotlinx.serialization.descriptors.SerialDescriptor - f:serialize(kotlinx.serialization.encoding.Encoder,com.intellij.ide.plugins.marketplace.IdeCompatibleUpdate):V +com.intellij.ide.settings.fileBasedSync.FileBasedSettingsSyncListProvider +- sf:Companion:com.intellij.ide.settings.fileBasedSync.FileBasedSettingsSyncListProvider$Companion +- a:getSettingsFileList():java.util.List +f:com.intellij.ide.settings.fileBasedSync.FileBasedSettingsSyncListProvider$Companion +- f:getEP_NAME():com.intellij.openapi.extensions.ExtensionPointName f:com.intellij.ide.startup.StartupActionScriptManager - s:addActionCommand(com.intellij.ide.startup.StartupActionScriptManager$ActionCommand):V - s:addActionCommands(java.util.List):V diff --git a/platform/platform-impl/src/com/intellij/ide/settings/fileBasedSync/FileBasedSettingsSyncListProvider.kt b/platform/platform-impl/src/com/intellij/ide/settings/fileBasedSync/FileBasedSettingsSyncListProvider.kt new file mode 100644 index 000000000000..54bc75a7c027 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/ide/settings/fileBasedSync/FileBasedSettingsSyncListProvider.kt @@ -0,0 +1,22 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.ide.settings.fileBasedSync + +import com.intellij.openapi.extensions.ExtensionPointName + +/** + * Allows specifying settings that should be synced between the monolith and backend as raw files + */ +interface FileBasedSettingsSyncListProvider { + + companion object { + val EP_NAME: ExtensionPointName = ExtensionPointName.Companion.create("com.intellij.rdserver.fileBasedSettingsSyncListProvider") + } + + /** + * Returns the list of the settings files that should be synced. + * + * **Note**: This method is called when the list is first required and every time the list of extensions changes. + */ + fun getSettingsFileList(): List + +} diff --git a/platform/projectModel-api/src/com/intellij/configurationStore/OperationListener.kt b/platform/projectModel-api/src/com/intellij/configurationStore/OperationListener.kt new file mode 100644 index 000000000000..a78cae9a7586 --- /dev/null +++ b/platform/projectModel-api/src/com/intellij/configurationStore/OperationListener.kt @@ -0,0 +1,29 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.configurationStore + +import org.jetbrains.annotations.ApiStatus + +/** + * Allows listening to changes in configuration files. + * + * See `com.intellij.configurationStore.StateStorageManagerImpl.addOperationListener`. + */ +@ApiStatus.Internal +interface OperationListener { + + /** + * Called when a configuration file is written + * + * @param fileSpec usually the name of the file + * @param content the content of the file + */ + fun onWrite(fileSpec: String, content: ByteArray) + + /** + * Called when a configuration file is deleted + * + * @param fileSpec usually the name of the file + */ + fun onDelete(fileSpec: String) + +} diff --git a/platform/projectModel-impl/src/com/intellij/configurationStore/StateStorageManager.kt b/platform/projectModel-impl/src/com/intellij/configurationStore/StateStorageManager.kt index 56e56dc74b5b..a9bdd53f65a2 100644 --- a/platform/projectModel-impl/src/com/intellij/configurationStore/StateStorageManager.kt +++ b/platform/projectModel-impl/src/com/intellij/configurationStore/StateStorageManager.kt @@ -23,6 +23,13 @@ interface StateStorageManager { fun removeStreamProvider(aClass: Class) + /** + * Adds a listener for changes in configuration files + * + * @param listener the listener + */ + fun addOperationListener(listener: OperationListener) {} + fun getOldStorage(component: Any, componentName: String, operation: StateStorageOperation): StateStorage? fun expandMacro(collapsedPath: String): Path