Skip to content

Commit

Permalink
Merge pull request #487 from FWDekker/docs-and-tests
Browse files Browse the repository at this point in the history
Docs and tests and stuff
  • Loading branch information
FWDekker authored Oct 22, 2023
2 parents 3375990 + e461cbc commit dbdde6c
Show file tree
Hide file tree
Showing 56 changed files with 415 additions and 279 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,17 @@ See [Plugin Signing](https://plugins.jetbrains.com/docs/intellij/plugin-signing.
```bash
$ gradlew test # Run tests (and collect coverage)
$ gradlew test --tests X # Run tests in class X (package name optional)
$ gradlew test -Pkotest.tags="X" # Run tests with `NamedTag` X (also supports not (!), and (&), or (|))
$ gradlew test -Pkotest.tags="X" # Run tests matching tag(s) X (also supports not (!), and (&), or (|))
$ gradlew koverHtmlReport # Create HTML coverage report for previous test run
$ gradlew check # Run tests and static analysis
$ gradlew runPluginVerifier # Check for compatibility issues
```

#### 🏷️ Filtering tests
#### 🏷️ Tagging and filtering tests
[Kotest tests can be tagged](https://kotest.io/docs/framework/tags.html) to allow selectively running tests.
The tags for Randomness are statically defined in
[`Tags`](https://github.com/FWDekker/intellij-randomness/blob/main/src/test/kotlin/com/fwdekker/randomness/testhelpers/Tags.kt).

Tag an entire test class by adding `tags(...)` to the class definition, or tag an individual test `context` by
writing `context("foo").config(tags = setOf(...)) {`.
It is not possible to tag an individual `test` due to limitations in Kotest.
Expand Down
23 changes: 12 additions & 11 deletions src/main/kotlin/com/fwdekker/randomness/Icons.kt
Original file line number Diff line number Diff line change
Expand Up @@ -85,17 +85,6 @@ data class TypeIcon(val base: Icon, val text: String, val colors: List<Color>) :
}


/**
* Returns an icon that describes both this icon's type and [other]'s type.
*/
fun combineWith(other: TypeIcon) =
TypeIcon(
Icons.TEMPLATE,
if (this.text == other.text) this.text else "",
this.colors + other.colors
)


/**
* Paints the colored text icon.
*
Expand Down Expand Up @@ -133,6 +122,18 @@ data class TypeIcon(val base: Icon, val text: String, val colors: List<Color>) :
* The scale of the text inside the icon relative to the icon's size.
*/
const val FONT_SIZE = 12f / 32f


/**
* Returns a single icon that describes all [icons], or `null` if [icons] is empty.
*/
fun combine(icons: Collection<TypeIcon>): TypeIcon? =
if (icons.isEmpty()) null
else TypeIcon(
Icons.TEMPLATE,
if (icons.map { it.text }.toSet().size == 1) icons.first().text else "",
icons.flatMap { it.colors }
)
}
}

Expand Down
62 changes: 39 additions & 23 deletions src/main/kotlin/com/fwdekker/randomness/PopupAction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class PopupAction : AnAction(Icons.RANDOMNESS) {
override fun update(event: AnActionEvent) {
event.presentation.icon = Icons.RANDOMNESS

// Running this in `actionPerformed` always sets it to `true`
// Running this in [actionPerformed] always sets it to `true`
isEditable = event.getData(CommonDataKeys.EDITOR)?.isViewer == false
}

Expand All @@ -54,7 +54,7 @@ class PopupAction : AnAction(Icons.RANDOMNESS) {
val popup = JBPopupFactory.getInstance()
.createActionGroupPopup(
Bundle("popup.title"), popupGroup, event.dataContext,
JBPopupFactory.ActionSelectionAid.NUMBERING,
JBPopupFactory.ActionSelectionAid.ALPHA_NUMBERING,
true
)
as ListPopupImpl
Expand Down Expand Up @@ -125,11 +125,25 @@ class PopupAction : AnAction(Icons.RANDOMNESS) {
Separator() +
TemplateSettingsAction()
}


/**
* Holds constants.
*/
companion object {
/**
* Returns the mnemonic for the entry at the [index]th row.
*
* @see JBPopupFactory.ActionSelectionAid.ALPHA_NUMBERING
*/
fun indexToMnemonic(index: Int) =
(('1'..'9') + '0' + ('A'..'Z')).getOrNull(index)
}
}


/**
* An `AbstractAction` that uses [myActionPerformed] as the implementation of its [actionPerformed] method.
* An [AbstractAction] that uses [myActionPerformed] as the implementation of its [actionPerformed] method.
*
* @property myActionPerformed The code to execute in [actionPerformed].
*/
Expand Down Expand Up @@ -201,26 +215,28 @@ fun ListPopupImpl.registerModifierActions(captionModifier: (ActionEvent?) -> Str
}
)

@Suppress("detekt:MagicNumber") // Not worth a constant
for (key in 0..9) {
registerAction(
"${a}${b}${c}invokeAction$key",
KeyStroke.getKeyStroke("$a $b $c pressed $key"),
SimpleAbstractAction { event ->
event ?: return@SimpleAbstractAction

val targetRow = if (key == 0) 9 else key - 1
list.addSelectionInterval(targetRow, targetRow)
handleSelect(
true,
KeyEvent(
component,
event.id, event.getWhen(), event.modifiers,
KeyEvent.VK_ENTER, KeyEvent.CHAR_UNDEFINED, KeyEvent.KEY_LOCATION_UNKNOWN
@Suppress("detekt:ForEachOnRange") // Not relevant for such small numbers
(0 until list.model.size)
.associateWith { PopupAction.indexToMnemonic(it) }
.filterValues { it != null }
.forEach { (index, mnemonic) ->
registerAction(
"${a}${b}${c}invokeAction$mnemonic",
KeyStroke.getKeyStroke("$a $b $c pressed $mnemonic"),
SimpleAbstractAction { event ->
event ?: return@SimpleAbstractAction

list.addSelectionInterval(index, index)
handleSelect(
true,
KeyEvent(
component,
event.id, event.getWhen(), event.modifiers,
KeyEvent.VK_ENTER, KeyEvent.CHAR_UNDEFINED, KeyEvent.KEY_LOCATION_UNKNOWN
)
)
)
}
)
}
}
)
}
}
}
18 changes: 9 additions & 9 deletions src/main/kotlin/com/fwdekker/randomness/XmlHelpers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,36 @@ import org.jdom.Element


/**
* Returns a list of all [Element]s contained in this `Element`.
* Returns a list of all [Element]s contained in this [Element].
*/
fun Element.getElements(): List<Element> =
content().toList().filterIsInstance<Element>()

/**
* Returns the [Element] contained in this `Element` that has attribute `name="[name]"`, or `null` if no single such
* `Element` exists.
* Returns the [Element] contained in this [Element] that has attribute `name="[name]"`, or `null` if no single such
* [Element] exists.
*/
fun Element.getContentByName(name: String): Element? =
getContent<Element> { (it as Element).getAttribute("name")?.value == name }.singleOrNull()

/**
* Returns the value of the `value` attribute of the single [Element] contained in this `Element` that has attribute
* `name="[name]"`, or `null` if no single such `Element` exists.
* Returns the value of the `value` attribute of the single [Element] contained in this [Element] that has attribute
* `name="[name]"`, or `null` if no single such [Element] exists.
*/
fun Element.getAttributeValueByName(name: String): String? =
getContentByName(name)?.getAttribute("value")?.value

/**
* Sets the value of the `value` attribute of the single [Element] contained in this `Element` that has attribute
* `name="[name]"` to [value], or does nothing if no single such `Element` exists.
* Sets the value of the `value` attribute of the single [Element] contained in this [Element] that has attribute
* `name="[name]"` to [value], or does nothing if no single such [Element] exists.
*/
fun Element.setAttributeValueByName(name: String, value: String) {
getContentByName(name)?.setAttribute("value", value)
}

/**
* Returns the single [Element] that is contained in this `Element`, or `null` if this `Element` does not contain
* exactly one `Element`.
* Returns the single [Element] that is contained in this [Element], or `null` if this [Element] does not contain
* exactly one [Element].
*/
fun Element.getSingleContent(): Element? =
content.singleOrNull() as? Element
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class DateTimeSchemeEditor(scheme: DateTimeScheme = DateTimeScheme()) : SchemeEd


/**
* Binds two `DateTimePicker`s together, analogous to how [com.fwdekker.randomness.ui.bindSpinners] works.
* Binds two [JDateTimeField]s together, analogous to how [com.fwdekker.randomness.ui.bindSpinners] works.
*/
private fun bindDateTimes(minField: JDateTimeField, maxField: JDateTimeField) {
addChangeListenerTo(minField) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ data class Template(
val arrayDecorator: ArrayDecorator = DEFAULT_ARRAY_DECORATOR,
) : Scheme() {
override val typeIcon
get() = schemes.mapNotNull { it.typeIcon }.reduceOrNull { acc, icon -> acc.combineWith(icon) } ?: DEFAULT_ICON
get() = TypeIcon.combine(schemes.mapNotNull { it.typeIcon }) ?: DEFAULT_ICON
override val decorators get() = listOf(arrayDecorator)

/**
Expand Down
76 changes: 35 additions & 41 deletions src/main/kotlin/com/fwdekker/randomness/template/TemplateJTree.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.fwdekker.randomness.template

import com.fwdekker.randomness.Bundle
import com.fwdekker.randomness.PopupAction
import com.fwdekker.randomness.Scheme
import com.fwdekker.randomness.datetime.DateTimeScheme
import com.fwdekker.randomness.decimal.DecimalScheme
Expand Down Expand Up @@ -28,8 +29,6 @@ import com.intellij.ui.treeStructure.Tree
import com.intellij.util.ui.JBUI
import javax.swing.JPanel
import javax.swing.JTree
import javax.swing.event.TreeExpansionEvent
import javax.swing.event.TreeWillExpandListener
import javax.swing.tree.TreeSelectionModel
import kotlin.math.min

Expand Down Expand Up @@ -80,7 +79,8 @@ class TemplateJTree(
/**
* The currently selected scheme (or template), or `null` if no scheme is currently selected.
*
* Setting a scheme that is either `null` or not in the tree will clear the current selection.
* Setting `null` or setting a [Scheme] with a UUID that does not occur in this tree will clear the current
* selection.
*/
var selectedScheme: Scheme?
get() = selectedNodeNotRoot?.state as? Scheme
Expand Down Expand Up @@ -114,15 +114,27 @@ class TemplateJTree(
}

/**
* UUIDs of templates that have explicitly been collapsed.
* The list of currently-collapsed [Template]s by UUID.
*
* Though a [TemplateJTree] knows which rows in the tree have been expanded and collapsed, it does not know by
* itself which templates these rows belong to. The tree would normally use the [myModel] to find this out, but that
* does not work if the UI and the model have become desynchronized, as is the case during [reload]. Therefore, the
* [collapsedNodes] field can be used even while desynchronized to determine which templates are collapsed, and,
* in particular, which templates should (not) be expanded after resynchronization.
* This field is rather finicky. It is used in [runPreservingState], which is used in [reload], which may be invoked
* while the UI and the [myModel] are desynchronized. Therefore, in the getter, this field uses [getUI] to access
* the (possibly outdated) UI to determine which [Template]s have currently been collapsed. In the setter,
* meanwhile, all [Template]s are expanded except the given [Template]s, to ensure that new [Template]s (which were
* added in such a way as to cause desynchronization) are expanded by default.
*/
private val collapsedNodes = currentTemplateList.templates.map { it.uuid }.toMutableSet()
private var collapsedTemplates: Collection<String>
get() {
val ui = getUI() ?: return emptyList()

return (0 until ui.getRowCount(this))
.mapNotNull { ui.getPathForRow(this, it) }
.filter { isCollapsed(it) }
.map { (it.lastPathComponent as StateNode).state.uuid }
}
set(value) =
myModel.root.children
.filter { it.state.uuid !in value }
.forEach { expandPath(myModel.getPathToRoot(it)) }


init {
Expand All @@ -133,15 +145,6 @@ class TemplateJTree(
showsRootHandles = true
selectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION
setCellRenderer(CellRenderer())
addTreeWillExpandListener(object : TreeWillExpandListener {
override fun treeWillExpand(event: TreeExpansionEvent) {
collapsedNodes -= (event.path.lastPathComponent as StateNode).state.uuid
}

override fun treeWillCollapse(event: TreeExpansionEvent) {
collapsedNodes += (event.path.lastPathComponent as StateNode).state.uuid
}
})

setSelectionRow(0)
}
Expand Down Expand Up @@ -183,24 +186,21 @@ class TemplateJTree(
* resynchronized. Otherwise, if [changedScheme] is `null`, the entire model is resynchronized.
*
* This is a wrapper around [TemplateJTreeModel.fireNodeStructureChanged] that additionally tries to retain the
* current selection and expansion state. Any new templates that have been added will initially be collapsed.
* current selection and expansion state. Any new templates that have been added will initially be expanded.
*
* @see runPreservingState
*/
fun reload(changedScheme: Scheme? = null) {
runPreservingState { myModel.fireNodeStructureChanged(changedScheme?.let { StateNode(it) } ?: myModel.root) }

selectedScheme = selectedScheme ?: currentTemplateList.templates.firstOrNull()
if (selectedScheme == null)
selectedScheme = currentTemplateList.templates.firstOrNull()
}

/**
* Expands the [Template]s identified by [uuids], and collapses all other [Template]s.
* Expands all [Template]s.
*/
fun expandNodes(uuids: Collection<String> = currentTemplateList.templates.map { it.uuid }) =
myModel.root.children
.associateWith { it.state.uuid in uuids }
.mapKeys { myModel.getPathToRoot(it.key) }
.forEach { (path, shouldExpand) -> setExpandedState(path, shouldExpand) }
fun expandAll() = myModel.root.children.forEach { expandPath(myModel.getPathToRoot(it)) }


/**
Expand Down Expand Up @@ -257,7 +257,7 @@ class TemplateJTree(
/**
* Replaces [oldScheme] with [newScheme] in-place.
*
* If `newScheme` is `null`, then `oldScheme` is removed without being replaced.
* If [newScheme] is `null`, then [oldScheme] is removed without being replaced.
*/
fun replaceScheme(oldScheme: Scheme, newScheme: Scheme?) {
if (newScheme == null) {
Expand Down Expand Up @@ -320,15 +320,15 @@ class TemplateJTree(


/**
* Runs [lambda] while ensuring that the [selectedScheme] and [collapsedNodes] remain unchanged.
* Runs [lambda] while ensuring that the [selectedScheme] and collapsed templates remain unchanged.
*/
private fun runPreservingState(lambda: () -> Unit) {
val oldSelected = selectedScheme
val oldCollapsed = collapsedNodes
val oldCollapsed = collapsedTemplates

lambda()

expandNodes(currentTemplateList.templates.map { it.uuid }.toSet() - oldCollapsed)
collapsedTemplates = oldCollapsed
selectedScheme = oldSelected
}

Expand Down Expand Up @@ -386,9 +386,8 @@ class TemplateJTree(
icon = scheme.icon

if (scheme is Template) {
val index = currentTemplateList.templates.indexOf(scheme) + 1
if (index <= INDEXED_TEMPLATE_COUNT)
append("${index % INDEXED_TEMPLATE_COUNT} ", SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES, false)
PopupAction.indexToMnemonic(currentTemplateList.templates.indexOf(scheme))
?.run { append("$this ", SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES, false) }
}

append(
Expand Down Expand Up @@ -467,12 +466,12 @@ class TemplateJTree(
/**
* Inserts [value] into the tree.
*
* Subclasses may modify the behavior of this method to instead return the `PopupStep` nested under this
* Subclasses may modify the behavior of this method to instead return the [PopupStep] nested under this
* entry.
*
* @param value the value to insert
* @param finalChoice ignored
* @return `null`, or the `PopupStep` that is nested under this entry
* @return `null`, or the [PopupStep] that is nested under this entry
*/
override fun onChosen(value: Scheme?, finalChoice: Boolean): PopupStep<*>? {
if (value != null)
Expand Down Expand Up @@ -691,10 +690,5 @@ class TemplateJTree(
DateTimeScheme(),
TemplateReference(),
)

/**
* Number of [Template]s that should be rendered with the index in front.
*/
const val INDEXED_TEMPLATE_COUNT = 10
}
}
Loading

0 comments on commit dbdde6c

Please sign in to comment.