Skip to content

Commit

Permalink
feat(ui): add ShireInputTextField component and integrate with ShireI…
Browse files Browse the repository at this point in the history
…nputSection #165

- Introduced `ShireInputTextField` for handling text input with document listeners and editor customization.
- Integrated `ShireInputTextField` into `ShireInputSection` with send and stop button icons.
- Added support for coroutine worker threads and updated MarketplaceView to handle input submission.
- Enhanced `ShireRunFileAction` with suspend execution functionality and added input validation messages.
  • Loading branch information
phodal committed Dec 19, 2024
1 parent cfd5cf4 commit 7f71c0d
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 133 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,7 @@ class ShireCoroutineScope {
companion object {
fun scope(project: Project): CoroutineScope =
project.getService(ShireCoroutineScope::class.java).coroutineScope

fun workerThread(): CoroutineScope = CoroutineScope(SupervisorJob() + workerThread)
}
}
138 changes: 8 additions & 130 deletions core/src/main/kotlin/com/phodal/shirecore/ui/ShireInputSection.kt
Original file line number Diff line number Diff line change
@@ -1,47 +1,32 @@
package com.phodal.shirecore.ui

import com.intellij.icons.AllIcons
import com.intellij.ide.IdeTooltip
import com.intellij.ide.IdeTooltipManager
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.*
import com.intellij.openapi.actionSystem.ex.AnActionListener
import com.intellij.openapi.actionSystem.impl.ActionButton
import com.intellij.openapi.command.CommandProcessor
import com.intellij.testFramework.LightVirtualFile
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.EditorModificationUtil
import com.intellij.openapi.editor.actions.EnterAction
import com.intellij.openapi.editor.actions.IncrementalFindAction
import com.intellij.openapi.editor.event.DocumentEvent
import com.intellij.openapi.editor.event.DocumentListener
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider
import com.intellij.openapi.fileTypes.FileTypes
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComponentValidator
import com.intellij.openapi.ui.popup.Balloon.Position
import com.intellij.openapi.wm.IdeFocusManager
import com.intellij.openapi.wm.impl.InternalDecorator
import com.intellij.ui.EditorTextField
import com.intellij.ui.HintHint
import com.intellij.util.EventDispatcher
import com.intellij.util.messages.MessageBusConnection
import com.intellij.util.ui.*
import com.intellij.util.ui.components.BorderLayoutPanel
import com.phodal.shirecore.ShireCoreBundle
import com.phodal.shirecore.sketch.highlight.findDocument
import com.phodal.shirecore.utils.markdown.CodeFenceLanguage
import java.awt.*
import java.awt.event.KeyEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.util.*
import javax.swing.Box
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.KeyStroke
import kotlin.math.max
import kotlin.math.min

Expand All @@ -56,114 +41,6 @@ interface ShireInputListener : EventListener {
fun onStop(component: ShireInputSection) {}
}

class ShireInputTextField(
project: Project,
private val listeners: List<DocumentListener>,
val disposable: Disposable?,
val inputSection: ShireInputSection,
) : EditorTextField(project, FileTypes.PLAIN_TEXT), Disposable {
private var editorListeners: EventDispatcher<ShireInputListener> = inputSection.editorListeners

init {
isOneLineMode = false
setFontInheritedFromLAF(true)
addSettingsProvider {
it.putUserData(IncrementalFindAction.SEARCH_DISABLED, true)
it.colorsScheme.lineSpacing = 1.2f
it.settings.isUseSoftWraps = true
it.isEmbeddedIntoDialogWrapper = true
it.contentComponent.setOpaque(false)
}

DumbAwareAction.create {
object : AnAction() {
override fun actionPerformed(actionEvent: AnActionEvent) {
val editor = editor ?: return
CommandProcessor.getInstance().executeCommand(project, {
val eol = "\n"
val caretOffset = editor.caretModel.offset
editor.document.insertString(caretOffset, eol)
editor.caretModel.moveToOffset(caretOffset + eol.length)
EditorModificationUtil.scrollToCaret(editor)
}, null, null)
}
}
}.registerCustomShortcutSet(
CustomShortcutSet(
KeyboardShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.CTRL_DOWN_MASK), null),
KeyboardShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.META_DOWN_MASK), null)
), this
)

val connect: MessageBusConnection = project.messageBus.connect(disposable ?: this)
val topic = AnActionListener.TOPIC
connect.subscribe(topic, object : AnActionListener {
override fun afterActionPerformed(action: AnAction, event: AnActionEvent, result: AnActionResult) {
if (event.dataContext.getData(CommonDataKeys.EDITOR) === editor && action is EnterAction) {
editorListeners.multicaster.onSubmit(inputSection, ShireInputTrigger.Key)
}
}
})

listeners.forEach { listener ->
document.addDocumentListener(listener)
}
}

override fun onEditorAdded(editor: Editor) {
editorListeners.multicaster.editorAdded((editor as EditorEx))
}

public override fun createEditor(): EditorEx {
val editor = super.createEditor()
editor.setVerticalScrollbarVisible(true)
setBorder(JBUI.Borders.empty())
editor.setShowPlaceholderWhenFocused(true)
editor.caretModel.moveToOffset(0)
editor.scrollPane.setBorder(border)
editor.contentComponent.setOpaque(false)
return editor
}

override fun getBackground(): Color {
val editor = editor ?: return super.getBackground()
return editor.colorsScheme.defaultBackground
}

override fun getData(dataId: String): Any? {
if (!PlatformCoreDataKeys.FILE_EDITOR.`is`(dataId)) {
return super.getData(dataId)
}

val currentEditor = editor ?: return super.getData(dataId)
return TextEditorProvider.getInstance().getTextEditor(currentEditor)
}

override fun dispose() {
listeners.forEach {
editor?.document?.removeDocumentListener(it)
}
}

fun recreateDocument() {
val id = UUID.randomUUID()
val language = CodeFenceLanguage.findLanguage("Shire")
val file = LightVirtualFile("ShireInput-$id", language, "")

val document = file.findDocument() ?: throw IllegalStateException("Can't create in-memory document")

initializeDocumentListeners(document)
setDocument(document)
inputSection.initEditor()
}

private fun initializeDocumentListeners(inputDocument: Document) {
listeners.forEach { listener ->
inputDocument.addDocumentListener(listener)
}
}
}

class ShireInputSection(private val project: Project, val disposable: Disposable?) : BorderLayoutPanel() {
private val input: ShireInputTextField
private val documentListener: DocumentListener
Expand All @@ -186,9 +63,11 @@ class ShireInputSection(private val project: Project, val disposable: Disposable

init {
val sendButtonPresentation = Presentation(ShireCoreBundle.message("chat.panel.send"))
sendButtonPresentation.icon = AllIcons.Actions.Execute
this.sendButtonPresentation = sendButtonPresentation

val stopButtonPresentation = Presentation("Stop")
stopButtonPresentation.icon = AllIcons.Actions.Suspend
this.stopButtonPresentation = stopButtonPresentation

sendButton = ActionButton(
Expand All @@ -199,9 +78,7 @@ class ShireInputSection(private val project: Project, val disposable: Disposable
}
}.actionPerformed(it)
},
this.sendButtonPresentation,
"",
Dimension(20, 20)
this.sendButtonPresentation, "", Dimension(20, 20)
)

stopButton = ActionButton(
Expand All @@ -212,9 +89,7 @@ class ShireInputSection(private val project: Project, val disposable: Disposable
}
}.actionPerformed(it)
},
this.stopButtonPresentation,
"",
Dimension(20, 20)
this.stopButtonPresentation, "", Dimension(20, 20)
)

input = ShireInputTextField(project, listOf(), disposable, this)
Expand Down Expand Up @@ -296,6 +171,9 @@ class ShireInputSection(private val project: Project, val disposable: Disposable
return result
}

/**
* Set the content of the input field.
*/
fun setContent(trimMargin: String) {
val focusManager = IdeFocusManager.getInstance(project)
focusManager.requestFocus(input, true)
Expand Down
136 changes: 136 additions & 0 deletions core/src/main/kotlin/com/phodal/shirecore/ui/ShireInputTextField.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package com.phodal.shirecore.ui

import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.*
import com.intellij.openapi.actionSystem.ex.AnActionListener
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.EditorModificationUtil
import com.intellij.openapi.editor.actions.EnterAction
import com.intellij.openapi.editor.actions.IncrementalFindAction
import com.intellij.openapi.editor.event.DocumentListener
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider
import com.intellij.openapi.fileTypes.FileTypes
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import com.intellij.testFramework.LightVirtualFile
import com.intellij.ui.EditorTextField
import com.intellij.util.EventDispatcher
import com.intellij.util.messages.MessageBusConnection
import com.intellij.util.ui.JBUI
import com.phodal.shirecore.sketch.highlight.findDocument
import com.phodal.shirecore.utils.markdown.CodeFenceLanguage
import java.awt.Color
import java.awt.event.KeyEvent
import java.util.*
import javax.swing.KeyStroke

class ShireInputTextField(
project: Project,
private val listeners: List<DocumentListener>,
val disposable: Disposable?,
val inputSection: ShireInputSection,
) : EditorTextField(project, FileTypes.PLAIN_TEXT), Disposable {
private var editorListeners: EventDispatcher<ShireInputListener> = inputSection.editorListeners

init {
isOneLineMode = false
setFontInheritedFromLAF(true)
addSettingsProvider {
it.putUserData(IncrementalFindAction.SEARCH_DISABLED, true)
it.colorsScheme.lineSpacing = 1.2f
it.settings.isUseSoftWraps = true
it.isEmbeddedIntoDialogWrapper = true
it.contentComponent.setOpaque(false)
}

DumbAwareAction.create {
object : AnAction() {
override fun actionPerformed(actionEvent: AnActionEvent) {
val editor = editor ?: return
CommandProcessor.getInstance().executeCommand(project, {
val eol = "\n"
val caretOffset = editor.caretModel.offset
editor.document.insertString(caretOffset, eol)
editor.caretModel.moveToOffset(caretOffset + eol.length)
EditorModificationUtil.scrollToCaret(editor)
}, null, null)
}
}
}.registerCustomShortcutSet(
CustomShortcutSet(
KeyboardShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.CTRL_DOWN_MASK), null),
KeyboardShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.META_DOWN_MASK), null)
), this
)

val connect: MessageBusConnection = project.messageBus.connect(disposable ?: this)
val topic = AnActionListener.TOPIC
connect.subscribe(topic, object : AnActionListener {
override fun afterActionPerformed(action: AnAction, event: AnActionEvent, result: AnActionResult) {
if (event.dataContext.getData(CommonDataKeys.EDITOR) === editor && action is EnterAction) {
editorListeners.multicaster.onSubmit(inputSection, ShireInputTrigger.Key)
}
}
})

listeners.forEach { listener ->
document.addDocumentListener(listener)
}
}

override fun onEditorAdded(editor: Editor) {
editorListeners.multicaster.editorAdded((editor as EditorEx))
}

public override fun createEditor(): EditorEx {
val editor = super.createEditor()
editor.setVerticalScrollbarVisible(true)
setBorder(JBUI.Borders.empty())
editor.setShowPlaceholderWhenFocused(true)
editor.caretModel.moveToOffset(0)
editor.scrollPane.setBorder(border)
editor.contentComponent.setOpaque(false)
return editor
}

override fun getBackground(): Color {
val editor = editor ?: return super.getBackground()
return editor.colorsScheme.defaultBackground
}

override fun getData(dataId: String): Any? {
if (!PlatformCoreDataKeys.FILE_EDITOR.`is`(dataId)) {
return super.getData(dataId)
}

val currentEditor = editor ?: return super.getData(dataId)
return TextEditorProvider.getInstance().getTextEditor(currentEditor)
}

override fun dispose() {
listeners.forEach {
editor?.document?.removeDocumentListener(it)
}
}

fun recreateDocument() {
val id = UUID.randomUUID()
val language = CodeFenceLanguage.findLanguage("Shire")
val file = LightVirtualFile("ShireInput-$id", language, "")

val document = file.findDocument() ?: throw IllegalStateException("Can't create in-memory document")

initializeDocumentListeners(document)
setDocument(document)
inputSection.initEditor()
}

private fun initializeDocumentListeners(inputDocument: Document) {
listeners.forEach { listener ->
inputDocument.addDocumentListener(listener)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.psi.PsiFileFactory
import com.phodal.shirecore.middleware.post.PostProcessorContext
import com.phodal.shirelang.ShireLanguage
import com.phodal.shirelang.actions.base.DynamicShireActionConfig
import com.phodal.shirelang.psi.ShireFile
import com.phodal.shirelang.run.*
Expand Down Expand Up @@ -89,6 +91,16 @@ class ShireRunFileAction : DumbAwareAction() {
ExecutionManager.getInstance(project).restartRunProfile(executionEnvironment)
}

fun suspendExecuteFile(
project: Project,
prompt: String,
): String? {
val shireFile: ShireFile = PsiFileFactory.getInstance(project)
.createFileFromText("temp.shire", ShireLanguage.INSTANCE, prompt) as ShireFile

return suspendExecuteFile(project, arrayOf(), mutableMapOf(), shireFile)
}

fun suspendExecuteFile(
project: Project,
variableNames: Array<String>,
Expand Down Expand Up @@ -134,7 +146,13 @@ class ShireRunFileAction : DumbAwareAction() {
val hintDisposable = Disposer.newDisposable()
val connection = ApplicationManager.getApplication().messageBus.connect(hintDisposable)
connection.subscribe(ShireRunListener.TOPIC, object : ShireRunListener {
override fun runFinish(allOutput: String, llmOutput: String, event: ProcessEvent, scriptPath: String, consoleView: ShireConsoleView?) {
override fun runFinish(
allOutput: String,
llmOutput: String,
event: ProcessEvent,
scriptPath: String,
consoleView: ShireConsoleView?,
) {
future.complete(llmOutput)
}
})
Expand Down
Loading

0 comments on commit 7f71c0d

Please sign in to comment.