Skip to content

Commit 72d0bf0

Browse files
committed
add option to store the .idea directory outside the project root (#191, #202)
1 parent c1328e9 commit 72d0bf0

6 files changed

Lines changed: 74 additions & 4 deletions

File tree

platform/configuration-store-impl/src/NestedProjectStorePathManager.kt

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
package com.intellij.configurationStore
33

44
import com.intellij.diagnostic.PluginException
5+
import com.intellij.ide.GeneralSettings
56
import com.intellij.ide.highlighter.ProjectFileType
67
import com.intellij.openapi.application.ApplicationManager
8+
import com.intellij.openapi.application.PathManager
79
import com.intellij.openapi.components.PersistentStateComponent
810
import com.intellij.openapi.components.State
911
import com.intellij.openapi.components.StateSplitterEx
@@ -23,12 +25,15 @@ import com.intellij.openapi.vfs.LocalFileSystem
2325
import com.intellij.openapi.vfs.VirtualFile
2426
import com.intellij.util.PathUtil
2527
import com.intellij.util.PathUtilRt
28+
import com.intellij.util.system.OS
2629
import org.jdom.Element
2730
import org.jetbrains.annotations.VisibleForTesting
2831
import org.jetbrains.jps.util.JpsPathUtil
2932
import java.nio.file.AccessDeniedException
3033
import java.nio.file.Files
3134
import java.nio.file.Path
35+
import java.nio.file.Paths
36+
import kotlin.io.path.div
3237

3338
private val EP_NAME: ExtensionPointName<ProjectStorePathCustomizer> = ExtensionPointName("com.intellij.projectStorePathCustomizer")
3439
private val DEPRECATED_PROJECT_FILE_STORAGE_ANNOTATION = FileStorageAnnotation(StoragePathMacros.PROJECT_FILE, true)
@@ -93,14 +98,35 @@ internal class NestedProjectStorePathManager : ProjectStorePathManager {
9398
}
9499
}
95100

101+
/**
102+
* used for embedding an absolute into another path, for example a path `/home/user/projects/foo` gets converted to `home/user/projects/foo`
103+
* and eventually becomes `/home/user/.config/detachhead/rebased/home/user/projects/foo` where `/home/user/.config/detachhead/rebased`
104+
* is the global IDE config directory.
105+
*
106+
* we do it this way so that projects can have their settings stored in a single spot outside their own root directory, and with predictable
107+
* locations.
108+
*/
109+
private fun Path.toRelativeWithNoDriveLetterSyntax(): Path {
110+
val pathString = this.toString()
111+
return Paths.get(
112+
if (OS.CURRENT == OS.Windows)
113+
pathString.replaceFirst(':', '\\')
114+
else
115+
pathString.replaceFirst("/", ""))
116+
}
117+
96118
private class DotIdeaProjectStoreDescriptor(
97119
override val projectIdentityFile: Path,
98120
override val historicalProjectBasePath: Path,
99121
) : ProjectStoreDescriptor {
100122
private var lastSavedProjectName: String? = null
101123

102124

103-
override val dotIdea: Path = projectIdentityFile.resolve(Project.DIRECTORY_STORE_FOLDER)
125+
override val dotIdea: Path =
126+
if (GeneralSettings.getInstance().storeProjectSettingsInProjectRoot)
127+
projectIdentityFile.resolve(Project.DIRECTORY_STORE_FOLDER)
128+
else
129+
(PathManager.getConfigDir() / Project.DIRECTORY_STORE_FOLDER).resolve(projectIdentityFile.toRelativeWithNoDriveLetterSyntax())
104130

105131
override fun getJpsBridgeAwareStorageSpec(filePath: String, project: Project): Storage {
106132
return doGetJpsBridgeAwareStorageSpec(filePath, project)

platform/configuration-store-impl/testSrc/DefaultProjectStoreTest.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
22
package com.intellij.configurationStore
33

4+
import com.intellij.ide.GeneralSettings
45
import com.intellij.ide.highlighter.ProjectFileType
56
import com.intellij.openapi.application.ApplicationManager
67
import com.intellij.openapi.application.EDT
8+
import com.intellij.openapi.application.PathManager
79
import com.intellij.openapi.application.ex.PathManagerEx
810
import com.intellij.openapi.components.Service
911
import com.intellij.openapi.components.impl.stores.stateStore
1012
import com.intellij.openapi.components.service
1113
import com.intellij.openapi.project.Project
1214
import com.intellij.openapi.project.ProjectManager
1315
import com.intellij.openapi.project.ex.ProjectManagerEx
16+
import com.intellij.openapi.project.impl.ProjectImpl
1417
import com.intellij.openapi.project.impl.ProjectManagerImpl
1518
import com.intellij.openapi.util.JDOMUtil
1619
import com.intellij.testFramework.ProjectRule
@@ -56,6 +59,13 @@ class DefaultProjectStoreTest {
5659
}
5760
}
5861

62+
@Test
63+
fun `project settings stored outside project root`(): Unit = runBlocking(Dispatchers.Default) {
64+
GeneralSettings.getInstance().storeProjectSettingsInProjectRoot = false
65+
val project = openAsNewProjectAndUseDefaultSettings(fsRule.fs.getPath("/test"))
66+
assertThat(project is ProjectImpl && project.componentStore.storeDescriptor.dotIdea!!.startsWith(PathManager.getConfigDir())).isTrue
67+
}
68+
5969
@Test
6070
fun `save default project configuration changes`() {
6171
runBlocking {

platform/external-system-impl/resources/META-INF/ExternalSystemExtensions.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@
118118
<!--Execution-->
119119
<programRunner implementation="com.intellij.openapi.externalSystem.service.execution.ExternalSystemTaskRunner"/>
120120

121-
<streamProviderFactory implementation="com.intellij.openapi.externalSystem.configurationStore.ExternalSystemStreamProviderFactory"/>
121+
<!--<streamProviderFactory implementation="com.intellij.openapi.externalSystem.configurationStore.ExternalSystemStreamProviderFactory"/>-->
122122

123123
<moduleService serviceInterface="com.intellij.openapi.externalSystem.ExternalSystemModulePropertyManager"
124124
serviceImplementation="com.intellij.openapi.externalSystem.service.project.ExternalSystemModulePropertyManagerBridge"/>

platform/ide-core/src/com/intellij/ide/GeneralSettings.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,15 @@ class GeneralSettings : PersistentStateComponent<GeneralSettingsState> {
134134
state.processCloseConfirmation = value
135135
}
136136

137+
// TODO: figure out why this doesn't work. it doesn't save the setting when changed for some reason
138+
// var storeProjectSettingsInProjectRoot by state::storeProjectSettingsInProjectRoot
139+
140+
var storeProjectSettingsInProjectRoot: Boolean
141+
get() = state.storeProjectSettingsInProjectRoot
142+
set(value) {
143+
state.storeProjectSettingsInProjectRoot = value
144+
}
145+
137146
init {
138147
val app = ApplicationManager.getApplication()
139148
if (app != null && !app.isHeadlessEnvironment &&
@@ -254,7 +263,9 @@ data class GeneralSettingsState(
254263
@JvmField
255264
var inactiveTimeout: Int = 15,
256265
@JvmField
257-
var supportScreenReaders: Boolean = false
266+
var supportScreenReaders: Boolean = false,
267+
@JvmField
268+
var storeProjectSettingsInProjectRoot: Boolean = true
258269
)
259270

260271
enum class ProcessCloseConfirmation {

platform/platform-api/resources/messages/IdeBundle.properties

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3285,3 +3285,7 @@ badge.text.new=New
32853285
badge.text.alpha=Alpha
32863286
badge.text.beta=Beta
32873287
badge.text.trial=Trial
3288+
3289+
# rebased-exclusive strings:
3290+
checkbox.store.project.settings.in.project.root=Store project settings in the project root directory
3291+
tooltip.store.project.settings.in.project.root=Disable this if you don''t want {0} to generate or respect <code>.idea</code> directories in the root of your projects.

platform/platform-impl/src/com/intellij/ide/GeneralSettingsConfigurable.kt

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package com.intellij.ide
33

44
import com.intellij.application.options.editor.CheckboxDescriptor
55
import com.intellij.application.options.editor.checkBox
6+
import com.intellij.ide.IdeBundle.message
67
import com.intellij.ide.ui.search.BooleanOptionDescription
8+
import com.intellij.openapi.application.ApplicationNamesInfo
79
import com.intellij.openapi.extensions.ExtensionPointName
810
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
911
import com.intellij.openapi.fileChooser.PathChooserDialog
@@ -29,6 +31,7 @@ import com.intellij.ui.dsl.builder.panel
2931
import com.intellij.ui.dsl.builder.selected
3032
import com.intellij.util.PlatformUtils
3133
import com.intellij.util.io.TrashBin
34+
import com.intellij.util.ui.RestartDialogImpl
3235

3336
private val model: GeneralSettings
3437
get() = GeneralSettings.getInstance()
@@ -49,6 +52,8 @@ private val myChkAutoSaveIfInactive
4952
get() = CheckboxDescriptor(IdeBundle.message("checkbox.save.files.automatically"), model::isAutoSaveIfInactive)
5053
private val myChkUseSafeWrite
5154
get() = CheckboxDescriptor(IdeBundle.message("checkbox.safe.write"), model::isUseSafeWrite)
55+
private val myChkStoreProjectSettingsInProjectRoot
56+
get() = CheckboxDescriptor(message("checkbox.store.project.settings.in.project.root"), model::storeProjectSettingsInProjectRoot)
5257

5358
internal val allOptionDescriptors: List<BooleanOptionDescription>
5459
get() =
@@ -60,7 +65,8 @@ internal val allOptionDescriptors: List<BooleanOptionDescription>
6065
myChkSyncInBackground,
6166
myChkSaveOnFrameDeactivation,
6267
myChkAutoSaveIfInactive,
63-
myChkUseSafeWrite
68+
myChkUseSafeWrite,
69+
myChkStoreProjectSettingsInProjectRoot
6470
)
6571
.map(CheckboxDescriptor::asUiOptionDescriptor)
6672

@@ -126,6 +132,11 @@ internal class GeneralSettingsConfigurable :
126132
.columns(COLUMNS_MEDIUM)
127133
.comment(IdeBundle.message("settings.general.directory.preselected"), 80)
128134
}
135+
row {
136+
checkBox(myChkStoreProjectSettingsInProjectRoot)
137+
.comment(message("ide.restart.required.comment"))
138+
.contextHelp(message("tooltip.store.project.settings.in.project.root", ApplicationNamesInfo.getInstance().productName))
139+
}
129140
}
130141

131142
group(IdeBundle.message("settings.general.files")) {
@@ -170,6 +181,14 @@ internal class GeneralSettingsConfigurable :
170181
}
171182
}
172183

184+
override fun apply() {
185+
val previousValue = model.storeProjectSettingsInProjectRoot
186+
super.apply()
187+
if (previousValue != model.storeProjectSettingsInProjectRoot) {
188+
RestartDialogImpl.showRestartRequired()
189+
}
190+
}
191+
173192
override fun getId(): String = helpTopic!!
174193

175194
override fun createConfigurables(): List<SearchableConfigurable> = ConfigurableWrapper.createConfigurables(EP_NAME)

0 commit comments

Comments
 (0)