Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docs and tests and stuff #487

Merged
merged 5 commits into from
Oct 22, 2023
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
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