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