Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@ package com.z8dn.plugins.a2pt.settings

import com.z8dn.plugins.a2pt.AndroidViewBundle

import com.intellij.icons.AllIcons
import com.intellij.ide.projectView.ProjectView
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.options.SearchableConfigurable
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.openapi.ui.popup.PopupStep
import com.intellij.openapi.ui.popup.util.BaseListPopupStep
import com.intellij.ui.ToolbarDecorator
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBLabel
Expand Down Expand Up @@ -74,6 +81,13 @@ class AndroidViewSettingsConfigurable : SearchableConfigurable {
.setAddAction { addGroup() }
.setRemoveAction { removeGroup() }
.setEditAction { editGroup() }
.addExtraAction(object : AnAction(
AndroidViewBundle.message("settings.LoadPreset.text"),
AndroidViewBundle.message("settings.LoadPreset.description"),
AllIcons.Actions.Download
) {
override fun actionPerformed(e: AnActionEvent) = loadPreset(e.dataContext)
})

panel.add(decorator.createPanel(), BorderLayout.CENTER)

Expand Down Expand Up @@ -108,6 +122,47 @@ class AndroidViewSettingsConfigurable : SearchableConfigurable {
}
}

/**
* Prompts the user to pick a preset, then a merge mode, and applies the result to the table.
*/
private fun loadPreset(dataContext: DataContext) {
val presetStep = object : BaseListPopupStep<FileGroupPreset>(
AndroidViewBundle.message("settings.LoadPreset.ChoosePreset.text"),
FileGroupPresets.ALL
) {
override fun getTextFor(value: FileGroupPreset): String = value.displayName

override fun onChosen(selectedValue: FileGroupPreset, finalChoice: Boolean): PopupStep<*>? {
return doFinalStep { chooseMergeMode(selectedValue, dataContext) }
}
}
JBPopupFactory.getInstance().createListPopup(presetStep).showInBestPositionFor(dataContext)
}

private fun chooseMergeMode(preset: FileGroupPreset, dataContext: DataContext) {
val modes = listOf(
PresetMergeMode.APPEND_SKIP_DUPLICATES to AndroidViewBundle.message("settings.LoadPreset.Mode.Append.text"),
PresetMergeMode.OVERWRITE_DUPLICATES to AndroidViewBundle.message("settings.LoadPreset.Mode.Overwrite.text"),
PresetMergeMode.REPLACE_ALL to AndroidViewBundle.message("settings.LoadPreset.Mode.Replace.text")
)
val modeStep = object : BaseListPopupStep<Pair<PresetMergeMode, String>>(
AndroidViewBundle.message("settings.LoadPreset.ChooseMode.text"),
modes
) {
override fun getTextFor(value: Pair<PresetMergeMode, String>): String = value.second

override fun onChosen(selectedValue: Pair<PresetMergeMode, String>, finalChoice: Boolean): PopupStep<*>? {
return doFinalStep { applyPreset(preset, selectedValue.first) }
}
}
JBPopupFactory.getInstance().createListPopup(modeStep).showInBestPositionFor(dataContext)
}

private fun applyPreset(preset: FileGroupPreset, mode: PresetMergeMode) {
val current = groupsTableModel?.getGroups() ?: emptyList()
groupsTableModel?.setGroups(FileGroupPresets.merge(current, preset, mode))
}

override fun isModified(): Boolean {
val settings = AndroidViewSettings.getInstance()

Expand Down
137 changes: 137 additions & 0 deletions src/main/kotlin/com/z8dn/plugins/a2pt/settings/FileGroupPresets.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package com.z8dn.plugins.a2pt.settings

import com.z8dn.plugins.a2pt.AndroidViewBundle

/**
* A named, predefined set of [ProjectFileGroup]s for a common project stack.
*
* Presets ship with the plugin and can be loaded into the Custom File Groups list from the
* settings page. Loading a preset produces ordinary [ProjectFileGroup] instances that remain
* freely editable afterwards.
*/
data class FileGroupPreset(
val displayName: String,
val groups: List<ProjectFileGroup>
)

/**
* How a preset should be merged into the existing list of file groups.
*/
enum class PresetMergeMode {
/** Add the preset's groups, skipping any whose name already exists (case-insensitive). */
APPEND_SKIP_DUPLICATES,

/** Add the preset's groups, replacing the patterns of any existing group with the same name. */
OVERWRITE_DUPLICATES,

/** Discard the existing groups entirely and use only the preset's groups. */
REPLACE_ALL
}

/**
* Built-in file-group presets for common stacks.
*/
object FileGroupPresets {

private fun group(name: String, vararg patterns: String): ProjectFileGroup =
ProjectFileGroup(groupName = name, patterns = patterns.toMutableList())

/** Standard Android project structure. */
private fun androidPreset(): FileGroupPreset = FileGroupPreset(
displayName = AndroidViewBundle.message("preset.Android.text"),
groups = listOf(
group(
"Gradle Scripts",
"build.gradle", "build.gradle.kts",
"settings.gradle", "settings.gradle.kts",
"gradle.properties", "libs.versions.toml", "*.versions.toml"
),
group(
"Android Config",
"AndroidManifest.xml", "proguard-rules.pro", "consumer-rules.pro", "*.pro"
),
group(
"Documentation",
"*.md", "LICENSE", "NOTICE", "*.txt"
)
)
)

/** Kotlin Multiplatform (KMP) project structure. */
private fun kmpPreset(): FileGroupPreset = FileGroupPreset(
displayName = AndroidViewBundle.message("preset.Kmp.text"),
groups = listOf(
group(
"Gradle Scripts",
"*.gradle.kts", "gradle.properties", "libs.versions.toml"
),
group(
"Native Configs",
"*.def", "*.podspec", "Podfile", "*.xcconfig"
),
group(
"Platform Metadata",
"*.toml", "*.properties"
)
)
)

/** Compose Multiplatform project structure. */
private fun composePreset(): FileGroupPreset = FileGroupPreset(
displayName = AndroidViewBundle.message("preset.Compose.text"),
groups = listOf(
group(
"Compose Config",
"*.gradle.kts", "compose.*", "gradle.properties"
),
group(
"Compose Resources",
"composeResources/*", "*.xml", "*.cdr"
)
)
)

/** All presets shipped with the plugin, in display order. */
val ALL: List<FileGroupPreset>
get() = listOf(androidPreset(), kmpPreset(), composePreset())

/**
* Merges [preset] into [existing] according to [mode], returning a new list.
* Group name comparison is case-insensitive.
*/
fun merge(
existing: List<ProjectFileGroup>,
preset: FileGroupPreset,
mode: PresetMergeMode
): List<ProjectFileGroup> {
fun copy(g: ProjectFileGroup) = ProjectFileGroup(g.groupName, g.patterns.toMutableList())

return when (mode) {
PresetMergeMode.REPLACE_ALL -> preset.groups.map { copy(it) }

PresetMergeMode.APPEND_SKIP_DUPLICATES -> {
val existingNames = existing.map { it.groupName.lowercase() }.toMutableSet()
val result = existing.map { copy(it) }.toMutableList()
preset.groups.forEach { g ->
if (existingNames.add(g.groupName.lowercase())) {
result.add(copy(g))
}
}
result
}

PresetMergeMode.OVERWRITE_DUPLICATES -> {
val result = existing.map { copy(it) }.toMutableList()
preset.groups.forEach { g ->
val index = result.indexOfFirst { it.groupName.equals(g.groupName, ignoreCase = true) }
if (index >= 0) {
result[index] = copy(g)
} else {
result.add(copy(g))
}
}
result
}
}
}
}
12 changes: 12 additions & 0 deletions src/main/resources/messages/AndroidViewBundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ settings.CustomFileGroups.text=Custom File Groups
settings.CustomFileGroups.description=Each group can contain multiple file patterns (e.g., *.md, LICENSE, README.md). Use ! to exclude files (e.g., !archive/*.md).
settings.Table.ColumnName.groupName=Group Name
settings.Table.ColumnName.patterns=Patterns
settings.LoadPreset.text=Load Preset
settings.LoadPreset.description=Add predefined file groups for a common project stack
settings.LoadPreset.ChoosePreset.text=Choose a Preset
settings.LoadPreset.ChooseMode.text=How to Apply Preset
settings.LoadPreset.Mode.Append.text=Append (skip duplicate names)
settings.LoadPreset.Mode.Overwrite.text=Overwrite groups with the same name
settings.LoadPreset.Mode.Replace.text=Replace all existing groups

# Presets
preset.Android.text=Standard Android
preset.Kmp.text=Kotlin Multiplatform
preset.Compose.text=Compose Multiplatform

# Project File Group Dialog
dialog.ProjectFileGroup.Add.text=Add Custom File Group
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.z8dn.plugins.a2pt.settings

import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test

class FileGroupPresetsTest {

private fun preset(name: String, vararg groups: ProjectFileGroup) =
FileGroupPreset(name, groups.toList())

private fun group(name: String, vararg patterns: String) =
ProjectFileGroup(name, patterns.toMutableList())

// region preset content

@Test
fun `every shipped preset has at least one group`() {
FileGroupPresets.ALL.forEach { p ->
assertTrue("preset '${p.displayName}' should have groups", p.groups.isNotEmpty())
}
}

@Test
fun `every group has a name and at least one inclusion pattern`() {
FileGroupPresets.ALL.forEach { p ->
p.groups.forEach { g ->
assertTrue("group name should not be blank in '${p.displayName}'", g.groupName.isNotBlank())
assertTrue("group '${g.groupName}' should have patterns", g.patterns.isNotEmpty())
assertTrue(
"group '${g.groupName}' should have at least one inclusion pattern",
g.patterns.any { !it.startsWith("!") }
)
}
}
}

@Test
fun `group names are unique within each preset`() {
FileGroupPresets.ALL.forEach { p ->
val names = p.groups.map { it.groupName.lowercase() }
assertEquals("group names should be unique in '${p.displayName}'", names.size, names.toSet().size)
}
}

// endregion

// region merge

@Test
fun `replace all discards existing groups`() {
val existing = listOf(group("Existing", "*.kt"))
val p = preset("P", group("A", "*.md"))

val result = FileGroupPresets.merge(existing, p, PresetMergeMode.REPLACE_ALL)

assertEquals(listOf("A"), result.map { it.groupName })
}

@Test
fun `append skips groups whose name already exists case-insensitively`() {
val existing = listOf(group("Docs", "*.txt"))
val p = preset("P", group("docs", "*.md"), group("New", "*.kt"))

val result = FileGroupPresets.merge(existing, p, PresetMergeMode.APPEND_SKIP_DUPLICATES)

assertEquals(listOf("Docs", "New"), result.map { it.groupName })
// existing 'Docs' patterns preserved, not overwritten
assertEquals(mutableListOf("*.txt"), result.first { it.groupName == "Docs" }.patterns)
}

@Test
fun `overwrite replaces patterns of duplicate names and appends the rest`() {
val existing = listOf(group("Docs", "*.txt"))
val p = preset("P", group("docs", "*.md"), group("New", "*.kt"))

val result = FileGroupPresets.merge(existing, p, PresetMergeMode.OVERWRITE_DUPLICATES)

assertEquals(listOf("Docs", "New"), result.map { it.groupName })
assertEquals(mutableListOf("*.md"), result.first { it.groupName == "Docs" }.patterns)
}

@Test
fun `merge does not mutate the source lists`() {
val existing = listOf(group("Docs", "*.txt"))
val p = preset("P", group("docs", "*.md"))

FileGroupPresets.merge(existing, p, PresetMergeMode.OVERWRITE_DUPLICATES)

assertEquals(mutableListOf("*.txt"), existing.first().patterns)
assertEquals(mutableListOf("*.md"), p.groups.first().patterns)
}

// endregion
}
Loading