[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
This commit is contained in:
Nikita Zaytsev
2025-10-14 19:46:31 +00:00
committed by intellij-monorepo-bot
parent 91a48987b8
commit 69bace9d1b
9 changed files with 128 additions and 16 deletions
@@ -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<OperationListener>()
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)
}
}
@@ -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
@@ -187,6 +187,7 @@ private class ModuleStateStorageManager(macroSubstitutor: TrackingPathMacroSubst
roamingType = roamingType,
pathMacroManager = macroSubstitutor,
provider = provider,
listener = null,
controller = null,
)
}
@@ -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)
@@ -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<StateMap>() {
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<VFileEvent>?) {
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<VFileEvent>?) {
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?) {
+5
View File
@@ -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
@@ -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<FileBasedSettingsSyncListProvider> = 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<String>
}
@@ -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)
}
@@ -23,6 +23,13 @@ interface StateStorageManager {
fun removeStreamProvider(aClass: Class<out StreamProvider>)
/**
* 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