Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# Changelog
## 3.3.5 -- 2025-TODO-TODO
TODO


## 3.3.4 -- 2025-01-17
### Changed
* Refreshed (encrypted) bug reporter token. ([#548](https://github.com/FWDekker/intellij-randomness/issues/548))
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
group=com.fwdekker
# Version number should also be updated in `com.fwdekker.randomness.PersistentSettings.Companion.CURRENT_VERSION`.
version=3.3.4
version=3.3.5

# Compatibility
# * `pluginSinceBuild`:
Expand Down
36 changes: 25 additions & 11 deletions src/main/kotlin/com/fwdekker/randomness/CapitalizationMode.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.fwdekker.randomness

import java.util.Locale
import kotlin.random.Random


Expand All @@ -23,12 +22,12 @@ enum class CapitalizationMode(val transform: (String, Random) -> String) {
/**
* Makes all characters uppercase.
*/
UPPER({ string, _ -> string.uppercase(Locale.getDefault()) }),
UPPER({ string, _ -> string.uppercase() }),

/**
* Makes all characters lowercase.
*/
LOWER({ string, _ -> string.lowercase(Locale.getDefault()) }),
LOWER({ string, _ -> string.lowercase() }),

/**
* Makes the first letter of each word uppercase.
Expand All @@ -45,20 +44,35 @@ enum class CapitalizationMode(val transform: (String, Random) -> String) {
/**
* Returns the localized string name of this mode.
*/
fun toLocalizedString() =
Bundle("shared.capitalization.${toString().replace(' ', '_').lowercase(Locale.getDefault())}")
fun toLocalizedString() = Bundle("shared.capitalization.${toString().replace(' ', '_').lowercase()}")
}


/**
* Randomly converts this character to uppercase or lowercase using [random] as a source of randomness.
* @see CapitalizationMode.RANDOM
*/
private fun Char.toRandomCase(random: Random) =
if (random.nextBoolean()) this.lowercaseChar()
else this.uppercaseChar()
if (random.nextBoolean()) lowercaseChar()
else uppercaseChar()

/**
* Turns the first character uppercase while all other characters become lowercase.
* @see CapitalizationMode.SENTENCE
*/
private fun String.toSentenceCase() =
this.lowercase(Locale.getDefault()).replaceFirstChar { it.uppercaseChar() }
private fun String.toSentenceCase() = lowercase().replaceFirstChar { it.uppercaseChar() }

/**
* Turns the first character lowercase while all other characters remain unchanged.
*/
fun String.lowerCaseFirst() = take(1).lowercase() + drop(1)

/**
* Turns the first character uppercase while all other characters remain unchanged.
*/
fun String.upperCaseFirst() = take(1).uppercase() + drop(1)

/**
* Appends [other], ensuring the resulting string is in camel case as long as [this] and [other] are also in camel case.
*/
fun String.camelPlus(other: String) =
if (this == "") other
else this + other.upperCaseFirst()
6 changes: 6 additions & 0 deletions src/main/kotlin/com/fwdekker/randomness/ListHelpers.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package com.fwdekker.randomness


/**
* Returns the [index]th element of [this], wrapping around circularly so that index `-1` is equivalent to
* `this.size - 1`.
*/
fun <E> List<E>.getMod(index: Int): E = this[(index % size + size) % size]

/**
* Removes all elements from this collection and adds all elements from [collection].
*/
Expand Down
5 changes: 2 additions & 3 deletions src/main/kotlin/com/fwdekker/randomness/PopupAction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.ui.popup.list.ListPopupImpl
import java.awt.event.ActionEvent
import java.awt.event.KeyEvent
import java.util.Locale
import javax.swing.AbstractAction
import javax.swing.KeyStroke

Expand Down Expand Up @@ -190,12 +189,12 @@ fun ListPopupImpl.registerModifierActions(captionModifier: (ActionEvent?) -> Str
(modifiers * optionalModifiers * optionalModifiers).forEach { (a, b, c) ->
registerAction(
"${a}${b}${c}Released",
KeyStroke.getKeyStroke("$b $c released ${a.uppercase(Locale.getDefault())}"),
KeyStroke.getKeyStroke("$b $c released ${a.uppercase()}"),
SimpleAbstractAction { setCaption(captionModifier(it)) }
)
registerAction(
"${a}${b}${c}Pressed",
KeyStroke.getKeyStroke("$a $b $c pressed ${a.uppercase(Locale.getDefault())}"),
KeyStroke.getKeyStroke("$a $b $c pressed ${a.uppercase()}"),
SimpleAbstractAction { setCaption(captionModifier(it)) }
)
registerAction(
Expand Down
5 changes: 2 additions & 3 deletions src/main/kotlin/com/fwdekker/randomness/Scheme.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,11 @@ abstract class Scheme : State() {
* The icon signifying the type of data represented by this scheme, ignoring its [decorators], or `null` if this
* scheme does not represent any kind of data, as is the case for [DecoratorScheme]s.
*/
@get:Transient
open val typeIcon: TypeIcon? = null

/**
* The icon signifying this scheme in its entirety, or `null` if it does not have an icon.
*/
@get:Transient
open val icon: OverlayedIcon? get() = typeIcon?.let { OverlayedIcon(it, decorators.mapNotNull(Scheme::icon)) }

/**
Expand All @@ -41,7 +39,6 @@ abstract class Scheme : State() {
* [com.intellij.util.xmlb.annotations.OptionTag]. This way, the deserializer knows that the field is not transient
* despite not being a mutable field.
*/
@get:Transient
abstract val decorators: List<DecoratorScheme>

/**
Expand Down Expand Up @@ -102,11 +99,13 @@ abstract class DecoratorScheme : Scheme() {
* Whether this decorator is enabled, or whether any invocation of [generateStrings] should be passed directly to
* the [generator].
*/
@get:Transient
protected open val isEnabled: Boolean = true

/**
* The generating function whose output should be decorated.
*/
@field:Transient
@get:Transient
lateinit var generator: (Int) -> List<String>

Expand Down
49 changes: 34 additions & 15 deletions src/main/kotlin/com/fwdekker/randomness/Settings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.SettingsCategory
import com.intellij.openapi.components.Storage
import com.intellij.openapi.components.service
import com.intellij.util.xmlb.XmlSerializer
import com.intellij.util.xmlb.XmlSerializer.deserialize
import com.intellij.util.xmlb.XmlSerializer.serialize
import com.intellij.util.xmlb.annotations.OptionTag
import com.intellij.util.xmlb.annotations.Transient
import org.jdom.Element
import java.lang.module.ModuleDescriptor.Version
import com.intellij.openapi.components.State as JBState
Expand All @@ -29,7 +29,6 @@ data class Settings(
/**
* @see TemplateList.templates
*/
@get:Transient
val templates: MutableList<Template> get() = templateList.templates


Expand Down Expand Up @@ -89,30 +88,35 @@ internal class PersistentSettings : PersistentStateComponent<Element> {
/**
* Returns the [settings] as an [Element].
*/
override fun getState(): Element = XmlSerializer.serialize(settings)
override fun getState(): Element = serialize(settings)

/**
* Deserializes [element] into a [Settings] instance, which is then stored in [settings].
*/
override fun loadState(element: Element) {
settings = XmlSerializer.deserialize(upgrade(element), Settings::class.java)
settings = deserialize(upgrade(element), Settings::class.java)
}


/**
* Silently upgrades the format of the settings contained in [element] to the format of the latest version.
* Upgrades the format of the settings contained in [element] to the format of the [targetVersion].
*
* @see UPGRADES
*/
private fun upgrade(element: Element): Element {
internal fun upgrade(element: Element, targetVersion: Version = Version.parse(CURRENT_VERSION)): Element {
require(targetVersion >= Version.parse("3.0.0")) { "Unsupported upgrade target version $targetVersion." }

val elementVersion = element.getPropertyValue("version")?.let { Version.parse(it) }
requireNotNull(elementVersion) { "Missing version number in Randomness settings." }
require(elementVersion >= Version.parse("3.0.0")) { "Unsupported Randomness config version $elementVersion." }

if (targetVersion <= elementVersion) return element

when {
elementVersion == null -> Unit
elementVersion < Version.parse("3.0.0") -> error("Unsupported Randomness config version $elementVersion.")
elementVersion < Version.parse("3.2.0") ->
element.getSchemes().filter { it.name == "UuidScheme" }.forEach { it.renameProperty("type", "version") }
}
UPGRADES
.filterKeys { elementVersion < it && targetVersion >= it }
.forEach { (_, it) -> it(element) }

element.setPropertyValue("version", CURRENT_VERSION)
element.setPropertyValue("version", targetVersion.toString())
return element
}

Expand All @@ -124,6 +128,21 @@ internal class PersistentSettings : PersistentStateComponent<Element> {
/**
* The currently-running version of Randomness.
*/
const val CURRENT_VERSION: String = "3.3.4" // Synchronize this with the version in `gradle.properties`
const val CURRENT_VERSION: String = "3.3.5" // Synchronize this with the version in `gradle.properties`

/**
* The upgrade functions to apply to configuration [Element]s in the [upgrade] method.
*/
private val UPGRADES: Map<Version, (Element) -> Unit> =
mapOf(
Version.parse("3.2.0") to
{ e: Element ->
e.getSchemes()
.filter { it.name == "UuidScheme" }
.forEach { it.renameProperty("type", "version") }
},
Version.parse("3.3.5") to
{ e: Element -> e.getDecorators().forEach { it.removeProperty("generator") } },
)
}
}
19 changes: 19 additions & 0 deletions src/main/kotlin/com/fwdekker/randomness/State.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@ import kotlin.random.asJavaRandom
* field `val list: MutableList<String>`. This reflects the typical case in which the user does not change the reference
* to the object, but changes the properties inside the object. And, more importantly, means that nested a
* [SchemeEditor] can share a single unchanging reference to a [State] with its nested [SchemeEditor]s.
*
* Properties should be annotated with [Transient] (not [kotlin.jvm.Transient]) to ensure they are not stored. The
* specifics of how the annotation is applied and inherited are complicated. The following rules apply:
* - Immutable non-[Collection]s (e.g. `val foo: Int` or `val bar: State`) are not serialized. Serialization annotations
* are ignored.
* - Immutable [Collection]s (e.g. `val foo: List<Int>` or `val bar: List<State>`) are not serialized, unless annotated
* with a serialization annotation such as [com.intellij.util.xmlb.annotations.XCollection] or
* [com.intellij.util.xmlb.annotations.OptionTag].
* - Mutable properties (i.e. `var`) are serialized, unless annotated with [Transient].
* - Do not combine [Transient] with any other serialization annotations.
* - To annotate a mutable property (i.e. `var`), use `@get:Transient`.
* - To annotate a lateinit property (i.e. `lateinit var`), use both `@field:Transient` and `@get:Transient`.
* - When a property overrides a property from a superclass, only the annotations in the subclass are relevant.
* - Since `abstract` properties must always be overridden, they should be annotated only in the subclass. Adding
* additional annotations "for clarity" in a superclass should be avoided, since it is completely useless and
* therefore misleading.
* - Since `open` properties are not always overridden, they should be annotated in the superclass and the subclass.
* - See also `isSerialized` in `StateReflection`.
*/
abstract class State {
/**
Expand All @@ -30,6 +48,7 @@ abstract class State {
*
* @see applyContext
*/
@field:Transient
@get:Transient
var context: Box<Settings> = Box({ Settings.DEFAULT })
protected set
Expand Down
30 changes: 30 additions & 0 deletions src/main/kotlin/com/fwdekker/randomness/XmlHelpers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ fun Element.renameProperty(oldName: String, newName: String) {
getMultiProperty(oldName).forEach { it.setAttribute("name", newName) }
}

/**
* Removes the property with attribute `name="[name]"`.
*/
fun Element.removeProperty(name: String) {
getMultiProperty(name).forEach { children.remove(it) }
}



/**
* Assuming this is the [Element] representation of a [Settings] instance, returns the [Element] of the contained
Expand All @@ -88,3 +96,25 @@ fun Element.getTemplates(): List<Element> =
*/
fun Element.getSchemes(): List<Element> =
getTemplates().mapNotNull { it.getProperty("schemes") }.flatMap { it.children }

/**
* Assuming this is the [Element] representation of a [Settings] instance, returns the list of [Element]s of the
* contained [com.fwdekker.randomness.DecoratorScheme]s. Searches recursively, so also returns decorators of decorators,
* and so on.
*/
fun Element.getDecorators(): List<Element> {
val decorators = mutableListOf<Element>()

var schemes = getSchemes()
while (schemes.isNotEmpty()) {
val containedDecorators = schemes.flatMap { scheme ->
scheme.children
.filter { child -> child.getAttribute("name")?.value.let { it != null && it.endsWith("Decorator") } }
.map { it.children.single() }
}
schemes = containedDecorators
decorators += containedDecorators
}

return decorators
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.fwdekker.randomness.affix

import com.fwdekker.randomness.Bundle
import com.fwdekker.randomness.SchemeEditor
import com.fwdekker.randomness.camelPlus
import com.fwdekker.randomness.ui.bindCurrentText
import com.fwdekker.randomness.ui.disableMnemonic
import com.fwdekker.randomness.ui.isEditable
Expand All @@ -13,7 +14,6 @@ import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.dsl.builder.selected
import com.intellij.ui.layout.ComponentPredicate
import com.intellij.ui.layout.and
import java.util.Locale
import javax.swing.JCheckBox


Expand All @@ -39,24 +39,16 @@ class AffixDecoratorEditor(

checkBox(Bundle("affix.ui.option"))
.let { if (enableMnemonic) it.loadMnemonic() else it.disableMnemonic() }
.withName(camelConcat(namePrefix, "affixEnabled"))
.withName(namePrefix.camelPlus("affixEnabled"))
.bindSelected(scheme::enabled)
.also { enabledCheckBox = it }

comboBox(presets)
.enabledIf(enabledIf?.and(enabledCheckBox.selected) ?: enabledCheckBox.selected)
.isEditable(true)
.withName(camelConcat(namePrefix, "affixDescriptor"))
.withName(namePrefix.camelPlus("affixDescriptor"))
.bindCurrentText(scheme::descriptor)
contextHelp(Bundle("affix.ui.comment"))
}.also { if (enabledIf != null) it.enabledIf(enabledIf) }
}
}


/**
* Prefixes [name] with [prefix], ensuring the resulting string is still in camelCase.
*/
private fun camelConcat(prefix: String, name: String) =
if (prefix == "") name
else "${prefix}${name[0].uppercase(Locale.getDefault())}${name.drop(1)}"
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import com.fwdekker.randomness.array.ArrayDecorator
import com.fwdekker.randomness.fixedlength.FixedLengthDecorator
import com.intellij.ui.JBColor
import com.intellij.util.xmlb.annotations.OptionTag
import com.intellij.util.xmlb.annotations.Transient
import java.awt.Color
import java.text.DecimalFormat

Expand Down Expand Up @@ -39,7 +38,6 @@ data class IntegerScheme(
@OptionTag val affixDecorator: AffixDecorator = DEFAULT_AFFIX_DECORATOR,
@OptionTag val arrayDecorator: ArrayDecorator = DEFAULT_ARRAY_DECORATOR,
) : Scheme() {
@get:Transient
override val name = Bundle("integer.title")
override val typeIcon get() = BASE_ICON
override val decorators get() = listOf(fixedLengthDecorator, affixDecorator, arrayDecorator)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ data class TemplateReference(
@OptionTag val affixDecorator: AffixDecorator = DEFAULT_AFFIX_DECORATOR,
@OptionTag val arrayDecorator: ArrayDecorator = DEFAULT_ARRAY_DECORATOR,
) : Scheme() {
@get:Transient
override val name get() = template?.name?.let { "[$it]" } ?: Bundle("reference.title")
override val typeIcon get() = template?.typeIcon ?: DEFAULT_ICON
override val icon get() = OverlayedIcon(typeIcon, decorators.mapNotNull { it.icon } + OverlayIcon.REFERENCE)
Expand All @@ -37,6 +38,7 @@ data class TemplateReference(
/**
* The [Template] in the [context]'s [TemplateList] that contains this [TemplateReference].
*/
@get:Transient
val parent: Template
get() = (+context).templates.single { template -> template.schemes.any { it.uuid == uuid } }

Expand Down
Loading
Loading