Skip to content

Commit

Permalink
Add more settings to IJ Plugin UI (#503)
Browse files Browse the repository at this point in the history
Summary:
As discussed on Slack, this PR exposes the existing settings (`FormattingOptions`) in the IJ plugin UI. This brings the IJ plugin in line with the Gradle one.

There is also a migration from the old settings (`enabled` as a boolean) to the new ones (`enableKtfmt` as an enum) that allows us to drop a bunch of legacy code and use better, Kotlin-friendly APIs. I (manually) tested both migrating from old settings (enabled and disabled) and starting anew, and it works fine. I have already used a similar strategy in the detekt plugin, and it worked well there, too.

## The new Custom option

https://github.com/user-attachments/assets/215f7e05-86a5-46d1-9cd7-4495fd3395d1

When selecting the Custom option, users can freely choose a max line length, indent/continuation indent size, and whether ktfmt should be managing imports and trailing commas. The UI will display validation errors if the numeric values are invalid (not shown above). It also allows to use one of the presets as starting points for customizing, which I expect will be a pretty common use case — e.g., I may want to use Meta's style, but with longer lines.

Pull Request resolved: #503

Reviewed By: cortinico

Differential Revision: D61204964

Pulled By: hick209

fbshipit-source-id: 974baaa7118535b5aa08563b4c242c0e32535696
  • Loading branch information
rock3r authored and facebook-github-bot committed Aug 14, 2024
1 parent a1ffe3d commit 7fceb85
Show file tree
Hide file tree
Showing 5 changed files with 325 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,27 @@

package com.facebook.ktfmt.intellij

import com.facebook.ktfmt.format.FormattingOptions
import com.facebook.ktfmt.intellij.KtfmtSettings.EnabledState.Disabled
import com.facebook.ktfmt.intellij.KtfmtSettings.EnabledState.Enabled
import com.facebook.ktfmt.intellij.UiFormatterStyle.Custom
import com.facebook.ktfmt.intellij.UiFormatterStyle.Google
import com.facebook.ktfmt.intellij.UiFormatterStyle.KotlinLang
import com.facebook.ktfmt.intellij.UiFormatterStyle.Meta
import com.intellij.openapi.options.BoundSearchableConfigurable
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.DialogPanel
import com.intellij.ui.dsl.builder.BottomGap
import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.bindIntText
import com.intellij.ui.dsl.builder.bindItem
import com.intellij.ui.dsl.builder.bindSelected
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.layout.selected
import com.intellij.ui.layout.selectedValueMatches
import javax.swing.JCheckBox
import javax.swing.JTextField

@Suppress("DialogTitleCapitalization")
class KtfmtConfigurable(project: Project) :
Expand All @@ -48,14 +59,133 @@ class KtfmtConfigurable(project: Project) :
.component
}

lateinit var styleComboBox: ComboBox<UiFormatterStyle>
row {
comboBox(UiFormatterStyle.values().toList())
.label("Code style:")
.bindItem(
getter = { settings.uiFormatterStyle },
setter = { settings.uiFormatterStyle = it ?: UiFormatterStyle.Meta },
)
.enabledIf(enabledCheckbox.selected)
styleComboBox =
comboBox(listOf(Meta, Google, KotlinLang, Custom))
.label("Code style:")
.bindItem(
getter = { settings.uiFormatterStyle },
setter = { settings.uiFormatterStyle = it ?: Meta },
)
.enabledIf(enabledCheckbox.selected)
.component
}

group("Custom style") {
lateinit var maxLineLength: JTextField
row("Max line length:") {
maxLineLength =
textField()
.bindIntText(settings::customMaxLineLength)
.validatePositiveIntegerOrEmpty()
.component
}

lateinit var blockIndent: JTextField
row("Block indent size:") {
blockIndent =
textField()
.bindIntText(settings::customBlockIndent)
.validatePositiveIntegerOrEmpty()
.component
}

lateinit var continuationIndent: JTextField
row("Continuation indent size:") {
continuationIndent =
textField()
.bindIntText(settings::customContinuationIndent)
.validatePositiveIntegerOrEmpty()
.component
}

lateinit var manageTrailingCommas: JCheckBox
row {
manageTrailingCommas =
checkBox("Manage trailing commas")
.bindSelected(settings::customManageTrailingCommas)
.component
}

lateinit var removeUnusedImports: JCheckBox
row {
removeUnusedImports =
checkBox("Remove unused imports")
.bindSelected(settings::customRemoveUnusedImports)
.component
}
.bottomGap(BottomGap.SMALL)

row("Copy from:") {
// Note: updating must be done via the components, and not the settings,
// or the Kotlin DSL bindings will overwrite the values when applying
link(Meta.toString()) {
UiFormatterStyle.getStandardFormattingOptions(Meta)
.updateFields(
maxLineLength,
blockIndent,
continuationIndent,
manageTrailingCommas,
removeUnusedImports,
)
}
.component
.autoHideOnDisable = false

link(Google.toString()) {
UiFormatterStyle.getStandardFormattingOptions(Google)
.updateFields(
maxLineLength,
blockIndent,
continuationIndent,
manageTrailingCommas,
removeUnusedImports,
)
}
.component
.autoHideOnDisable = false

link(KotlinLang.toString()) {
UiFormatterStyle.getStandardFormattingOptions(KotlinLang)
.updateFields(
maxLineLength,
blockIndent,
continuationIndent,
manageTrailingCommas,
removeUnusedImports,
)
}
.component
.autoHideOnDisable = false
}
}
.visibleIf(styleComboBox.selectedValueMatches { it == Custom })
.enabledIf(enabledCheckbox.selected)
}
}

private fun FormattingOptions.updateFields(
maxLineLength: JTextField,
blockIndent: JTextField,
continuationIndent: JTextField,
manageTrailingCommas: JCheckBox,
removeUnusedImports: JCheckBox,
) {
maxLineLength.text = maxWidth.toString()
blockIndent.text = this.blockIndent.toString()
continuationIndent.text = this.continuationIndent.toString()
manageTrailingCommas.isSelected = this.manageTrailingCommas
removeUnusedImports.isSelected = this.removeUnusedImports
}

private fun Cell<JTextField>.validatePositiveIntegerOrEmpty() = validationOnInput { jTextField ->
if (jTextField.text.isNotEmpty()) {
val parsedValue = jTextField.text.toIntOrNull()
when {
parsedValue == null -> error("Value must be an integer. Will default to 1")
parsedValue <= 0 -> error("Value must be greater than zero. Will default to 1")
else -> null
}
} else null
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.facebook.ktfmt.intellij

import com.facebook.ktfmt.format.Formatter.format
import com.facebook.ktfmt.format.FormattingOptions
import com.google.googlejavaformat.java.FormatterException
import com.intellij.formatting.service.AsyncDocumentFormattingService
import com.intellij.formatting.service.AsyncFormattingRequest
Expand All @@ -31,8 +32,15 @@ private const val PARSING_ERROR_TITLE: String = PARSING_ERROR_NOTIFICATION_GROUP
class KtfmtFormattingService : AsyncDocumentFormattingService() {
override fun createFormattingTask(request: AsyncFormattingRequest): FormattingTask {
val project = request.context.project
val style = KtfmtSettings.getInstance(project).uiFormatterStyle
return KtfmtFormattingTask(request, style)
val settings = KtfmtSettings.getInstance(project)
val style = settings.uiFormatterStyle
val formattingOptions =
if (style == UiFormatterStyle.Custom) {
settings.customFormattingOptions
} else {
UiFormatterStyle.getStandardFormattingOptions(style)
}
return KtfmtFormattingTask(request, formattingOptions)
}

override fun getNotificationGroupId(): String = PARSING_ERROR_NOTIFICATION_GROUP
Expand All @@ -47,11 +55,11 @@ class KtfmtFormattingService : AsyncDocumentFormattingService() {

private class KtfmtFormattingTask(
private val request: AsyncFormattingRequest,
private val style: UiFormatterStyle,
private val formattingOptions: FormattingOptions,
) : FormattingTask {
override fun run() {
try {
val formattedText = format(style.formattingOptions, request.documentText)
val formattedText = format(formattingOptions, request.documentText)
request.onTextReady(formattedText)
} catch (e: FormatterException) {
request.onError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,45 +16,104 @@

package com.facebook.ktfmt.intellij

import com.facebook.ktfmt.format.Formatter
import com.facebook.ktfmt.format.FormattingOptions
import com.facebook.ktfmt.intellij.KtfmtSettings.EnabledState.Disabled
import com.facebook.ktfmt.intellij.KtfmtSettings.EnabledState.Enabled
import com.facebook.ktfmt.intellij.KtfmtSettings.EnabledState.Unknown
import com.facebook.ktfmt.intellij.UiFormatterStyle.Meta
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.BaseState
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.Service.Level.PROJECT
import com.intellij.openapi.components.SimplePersistentStateComponent
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.project.Project

@Service(PROJECT)
@State(name = "KtfmtSettings", storages = [Storage("ktfmt.xml")])
internal class KtfmtSettings : PersistentStateComponent<KtfmtSettings.State> {
private var state = State()

internal class KtfmtSettings(private val project: Project) :
SimplePersistentStateComponent<KtfmtSettings.State>(State()) {
val isUninitialized: Boolean
get() = state.enabled == Unknown
get() = state.enableKtfmt == Unknown

var uiFormatterStyle: UiFormatterStyle
get() = state.uiFormatterStyle
set(uiFormatterStyle) {
state.uiFormatterStyle = uiFormatterStyle
}

var customFormattingOptions: FormattingOptions
get() =
FormattingOptions(
state.customMaxLineLength,
state.customBlockIndent,
state.customContinuationIndent,
state.customManageTrailingCommas,
state.customRemoveUnusedImports,
)
set(customFormattingOptions) {
state.applyCustomFormattingOptions(customFormattingOptions)
}

var customMaxLineLength: Int
get() = state.customMaxLineLength
set(maxLineLength) {
state.customMaxLineLength = maxLineLength.coerceAtLeast(1)
}

var customBlockIndent: Int
get() = state.customBlockIndent
set(blockIndent) {
state.customBlockIndent = blockIndent.coerceAtLeast(1)
}

var customContinuationIndent: Int
get() = state.customContinuationIndent
set(continuationIndent) {
state.customContinuationIndent = continuationIndent.coerceAtLeast(1)
}

var customManageTrailingCommas: Boolean
get() = state.customManageTrailingCommas
set(manageTrailingCommas) {
state.customManageTrailingCommas = manageTrailingCommas
}

var customRemoveUnusedImports: Boolean
get() = state.customRemoveUnusedImports
set(removeUnusedImports) {
state.customRemoveUnusedImports = removeUnusedImports
}

var isEnabled: Boolean
get() = state.enabled == Enabled
get() = state.enableKtfmt == Enabled
set(enabled) {
setEnabled(if (enabled) Enabled else Disabled)
}

fun setEnabled(enabled: EnabledState) {
state.enabled = enabled
state.enableKtfmt = enabled
}

override fun getState(): State = state

override fun loadState(state: State) {
this.state = state
val migrated = loadOrMigrateIfNeeded(state)
super.loadState(migrated)
}

private fun loadOrMigrateIfNeeded(state: State): State {
val migrationSettings = project.service<KtfmtSettingsMigration>()

return when (val stateVersion = migrationSettings.stateVersion) {
KtfmtSettingsMigration.CURRENT_VERSION -> state
1 -> migrationSettings.migrateFromV1ToCurrent(state)
else -> {
thisLogger().error("Cannot migrate settings from $stateVersion. Using defaults.")
State()
}
}
}

internal enum class EnabledState {
Expand All @@ -63,27 +122,27 @@ internal class KtfmtSettings : PersistentStateComponent<KtfmtSettings.State> {
Disabled,
}

internal class State {
@JvmField var enabled: EnabledState = Unknown
var uiFormatterStyle: UiFormatterStyle = Meta

// enabled used to be a boolean so we use bean property methods for backwards
// compatibility
fun setEnabled(enabledStr: String?) {
enabled =
when {
enabledStr == null -> Unknown
enabledStr.toBoolean() -> Enabled
else -> Disabled
}
}
internal class State : BaseState() {
@Deprecated("Deprecated in V2. Use enableKtfmt instead.") var enabled by string()

var enableKtfmt by enum<EnabledState>(Unknown)
var uiFormatterStyle by enum<UiFormatterStyle>(Meta)

var customMaxLineLength by property(Formatter.META_FORMAT.maxWidth)
var customBlockIndent by property(Formatter.META_FORMAT.blockIndent)
var customContinuationIndent by property(Formatter.META_FORMAT.continuationIndent)
var customManageTrailingCommas by property(Formatter.META_FORMAT.manageTrailingCommas)
var customRemoveUnusedImports by property(Formatter.META_FORMAT.removeUnusedImports)

fun getEnabled(): String? =
when (enabled) {
Enabled -> "true"
Disabled -> "false"
else -> null
}
fun applyCustomFormattingOptions(formattingOptions: FormattingOptions) {
customMaxLineLength = formattingOptions.maxWidth
customBlockIndent = formattingOptions.blockIndent
customContinuationIndent = formattingOptions.continuationIndent
customManageTrailingCommas = formattingOptions.manageTrailingCommas
customRemoveUnusedImports = formattingOptions.removeUnusedImports

incrementModificationCount()
}
}

companion object {
Expand Down
Loading

0 comments on commit 7fceb85

Please sign in to comment.