Skip to content
Open
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
@@ -0,0 +1,99 @@
package ee.carlrobert.codegpt.actions.editor

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.*
import com.intellij.util.messages.Topic
import com.intellij.util.xmlb.XmlSerializerUtil
import com.intellij.util.xmlb.annotations.CollectionBean
import com.intellij.util.xmlb.annotations.Transient
import ee.carlrobert.codegpt.util.trimToSize
import kotlin.properties.Delegates

/**
* Persistent plugin states.
*/
@State(name = "CodeGPT_EditCodeHistory", storages = [(Storage("CodeGPT_EditCodeHistory.xml"))])
@Service
class EditCodeHistoryStates : PersistentStateComponent<EditCodeHistoryStates> {

@CollectionBean
private val histories: MutableList<String> = ArrayList(DEFAULT_HISTORY_SIZE)

var maxHistorySize by Delegates.vetoable(DEFAULT_HISTORY_SIZE) { _, oldValue: Int, newValue: Int ->
if (oldValue == newValue || newValue < 0) {
return@vetoable false
}

trimHistoriesSize(newValue)
true
}

@Transient
private val dataChangePublisher: HistoriesChangedListener =
ApplicationManager.getApplication().messageBus.syncPublisher(HistoriesChangedListener.TOPIC)

override fun getState(): EditCodeHistoryStates = this

override fun loadState(state: EditCodeHistoryStates) {
XmlSerializerUtil.copyBean(state, this)
}

private fun trimHistoriesSize(maxSize: Int) {
if (histories.trimToSize(maxSize)) {
dataChangePublisher.onHistoriesChanged()
}
}

fun getHistories(): List<String> = histories

fun addHistory(query: String) {
val maxSize = maxHistorySize
if (maxSize <= 0) {
return
}

histories.run {
val index = indexOf(query)
if (index != 0) {
if (index > 0) {
removeAt(index)
}

add(0, query)
trimToSize(maxSize)
dataChangePublisher.onHistoryItemChanged(query)
}
}
}

fun clearHistories() {
if (histories.isNotEmpty()) {
histories.clear()
dataChangePublisher.onHistoriesChanged()
}
}

companion object {
private const val DEFAULT_HISTORY_SIZE = 50

/**
* Get the instance of [EditCodeHistoryStates].
*/
fun getInstance(): EditCodeHistoryStates {
return service<EditCodeHistoryStates>().state
}
}
}

interface HistoriesChangedListener {

fun onHistoriesChanged()

fun onHistoryItemChanged(newHistory: String)

companion object {
@Topic.AppLevel
val TOPIC: Topic<HistoriesChangedListener> =
Topic.create("TranslateHistoriesChanged", HistoriesChangedListener::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package ee.carlrobert.codegpt.actions.editor

import com.intellij.openapi.util.text.StringUtil
import com.intellij.ui.SimpleListCellRenderer
import javax.swing.JList


class HistoryRenderer : SimpleListCellRenderer<String>() {

private val builder = StringBuilder()

override fun customize(
list: JList<out String>,
value: String?,
index: Int,
selected: Boolean,
hasFocus: Boolean
) {
if (list.width == 0 || value.isNullOrBlank()) {
text = null
} else {
setRenderText(value)
}
}


private fun setRenderText(value: String) {
val text = with(builder) {
setLength(0)


append("<html><body><b>")
append(trim(value))
append("</b>")

builder.append("</body></html>")
toString()
}
setText(text)
}

private fun trim(value: String?): String? {
value ?: return null

val withoutNewLines = StringUtil.convertLineSeparators(value, "")
return StringUtil.first(withoutNewLines, 100, /*appendEllipsis*/ true)
}
}
123 changes: 109 additions & 14 deletions src/main/kotlin/ee/carlrobert/codegpt/ui/EditCodePopover.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ee.carlrobert.codegpt.ui

import com.intellij.icons.AllIcons
import com.intellij.ide.IdeBundle
import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI
import com.intellij.openapi.actionSystem.ActionPlaces
Expand All @@ -11,29 +12,43 @@ import com.intellij.openapi.observable.properties.AtomicBooleanProperty
import com.intellij.openapi.observable.properties.ObservableProperty
import com.intellij.openapi.observable.util.not
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.openapi.ui.popup.JBPopupListener
import com.intellij.openapi.ui.popup.LightweightWindowEvent
import com.intellij.openapi.ui.popup.util.MinimizeButton
import com.intellij.openapi.util.IconLoader
import com.intellij.ui.DocumentAdapter
import com.intellij.ui.awt.RelativePoint
import com.intellij.ui.components.JBTextField
import com.intellij.ui.components.labels.LinkLabel
import com.intellij.ui.components.panels.NonOpaquePanel
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.Row
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.layout.ComponentPredicate
import com.intellij.ui.scale.JBUIScale
import com.intellij.util.IconUtil
import com.intellij.util.ui.AsyncProcessIcon
import com.intellij.util.ui.JBUI
import ee.carlrobert.codegpt.CodeGPTBundle
import ee.carlrobert.codegpt.actions.editor.EditCodeHistoryStates
import ee.carlrobert.codegpt.actions.editor.EditCodeSubmissionHandler
import ee.carlrobert.codegpt.settings.models.ModelSettings
import ee.carlrobert.codegpt.settings.models.SettingsModelComboBoxAction
import ee.carlrobert.codegpt.settings.service.FeatureType
import ee.carlrobert.codegpt.actions.editor.HistoryRenderer
import ee.carlrobert.codegpt.settings.GeneralSettings
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.ModelComboBoxAction
import ee.carlrobert.codegpt.util.ApplicationUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import net.miginfocom.layout.CC
import net.miginfocom.layout.LC
import net.miginfocom.swing.MigLayout
import java.awt.Dimension
import java.awt.Point
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import javax.swing.JButton
import javax.swing.JPanel
import javax.swing.*
import javax.swing.event.DocumentEvent

data class ObservableProperties(
Expand All @@ -47,6 +62,7 @@ class EditCodePopover(private val editor: Editor) {
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val observableProperties = ObservableProperties()
private val submissionHandler = EditCodeSubmissionHandler(editor, observableProperties)

private val promptTextField = JBTextField("", 40).apply {
emptyText.appendText(CodeGPTBundle.get("editCodePopover.textField.emptyText"))
addKeyListener(object : KeyAdapter() {
Expand All @@ -58,7 +74,37 @@ class EditCodePopover(private val editor: Editor) {
}
})
}
private val popup = JBPopupFactory.getInstance()

/**
* Code editing history storage
*/
private val states = EditCodeHistoryStates.getInstance();

/**
* History button, delete button
*/
private val historyButton: LinkLabel<Void> = LinkLabel<Void>().apply {
icon = AllIcons.Vcs.History
disabledIcon = IconLoader.getDisabledIcon(AllIcons.Vcs.History)
setHoveringIcon(IconUtil.darker(AllIcons.Vcs.History, 3))
toolTipText = "History"
setListener({ _, _ -> showHistoryPopup() }, null)
}
private val clearButton: LinkLabel<Void> = LinkLabel<Void>().apply {
icon = AllIcons.Actions.GC
disabledIcon = IconLoader.getDisabledIcon(AllIcons.Actions.GC)
setHoveringIcon(IconUtil.darker(AllIcons.Actions.GC, 3))
toolTipText = "Clear"
setListener({ _, _ ->
run {
promptTextField.text = null
}
}, null)
}

private var historyShowing: Boolean = false

val popup = JBPopupFactory.getInstance()
.createComponentPopupBuilder(
createPopupPanel(),
promptTextField
Expand All @@ -85,6 +131,11 @@ class EditCodePopover(private val editor: Editor) {
row {
cell(promptTextField)
}
row {
cell(createToolbar(clearButton, historyButton))
.align(AlignX.FILL)
}

row {
comment(CodeGPTBundle.get("editCodePopover.textField.comment"))
}
Expand Down Expand Up @@ -118,19 +169,59 @@ class EditCodePopover(private val editor: Editor) {
font = JBUI.Fonts.smallFont()
}
cell(
SettingsModelComboBoxAction(
FeatureType.EDIT_CODE,
ModelSettings.getInstance().getModelSelection(FeatureType.EDIT_CODE),
{}
).createCustomComponent(ActionPlaces.UNKNOWN)
ModelComboBoxAction(
ApplicationUtil.findCurrentProject(),
{},
GeneralSettings.getSelectedService()
)
.createCustomComponent(ActionPlaces.UNKNOWN)
).align(AlignX.RIGHT)
}
}.apply {
border = JBUI.Borders.empty(8, 8, 2, 8)
}
}

private fun Row.button(title: String, visibleIf: ObservableProperty<Boolean>): Cell<JButton> {
private fun createToolbar(vararg buttons: JComponent): JPanel {
return NonOpaquePanel(migLayout("4")).apply {
add(JPanel().apply { isOpaque = false }, CC().growX().pushX()) // Left glue
buttons.iterator().forEach {
add(it, CC().gapLeft("${JBUIScale.scale(4)}px"))
}
border = JBUI.Borders.empty(2, 0, 4, 0)
}
}

private fun showHistoryPopup() {
return JBPopupFactory.getInstance().createPopupChooserBuilder(states.getHistories())
.setVisibleRowCount(7)
.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
.setItemSelectedCallback { promptTextField.text = it }
.setRenderer(HistoryRenderer())
.addListener(object : JBPopupListener {
override fun beforeShown(event: LightweightWindowEvent) {
historyShowing = true
val popup = event.asPopup()
popup.size = Dimension(300, popup.size.height)
val relativePoint = RelativePoint(historyButton, Point(0, -JBUI.scale(3)))
val screenPoint = Point(relativePoint.screenPoint).apply { translate(0, -popup.size.height) }

popup.setLocation(screenPoint)
}

override fun onClosed(event: LightweightWindowEvent) {
historyShowing = false
}
})
.createPopup()
.show(historyButton)
}

fun migLayout(gapX: String = "0!", gapY: String = "0!", insets: String = "0", lcBuilder: (LC.() -> Unit)? = null) =
MigLayout(LC().fill().gridGap(gapX, gapY).insets(insets).also { lcBuilder?.invoke(it) })


fun Row.button(title: String, visibleIf: ObservableProperty<Boolean>): Cell<JButton> {
val button = JButton(title).apply {
putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true)
addActionListener {
Expand All @@ -150,8 +241,12 @@ class EditCodePopover(private val editor: Editor) {
}

private fun handleSubmit() {
val text = promptTextField.text
if (text.isNotBlank()) {
states.addHistory(text)
}
serviceScope.launch {
submissionHandler.handleSubmit(promptTextField.text)
submissionHandler.handleSubmit(text)
promptTextField.text = ""
promptTextField.emptyText.text =
CodeGPTBundle.get("editCodePopover.textField.followUp.emptyText")
Expand Down Expand Up @@ -193,4 +288,4 @@ class EditCodePopover(private val editor: Editor) {
}
}
}
}
}
19 changes: 19 additions & 0 deletions src/main/kotlin/ee/carlrobert/codegpt/util/ListConverter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package ee.carlrobert.codegpt.util

import com.fasterxml.jackson.core.type.TypeReference

class ListConverter : BaseConverter<List<Any>>(object : TypeReference<List<Any>>() {})

/**
* Trims the [MutableList] to [maxSize]
*/
fun <T> MutableList<T>.trimToSize(maxSize: Int): Boolean {
var size = this.size
val trim = size > 0 && size > maxSize
when {
trim && maxSize <= 0 -> clear()
trim -> while (size > maxSize) removeAt(--size)
}

return trim
}
Loading