diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c6226702..f4f131f2 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -43,7 +43,7 @@ jobs:
workflow: node.js.yml
workflow_search: true
repo: smallcloudai/refact-chat-js
- branch: alpha
+ branch: main
name: refact-chat-js-latest
path: ./src/main/resources/webview/dist
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index dde33f63..e9fedf6d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -71,7 +71,7 @@ jobs:
workflow: node.js.yml
workflow_search: true
repo: smallcloudai/refact-chat-js
- branch: alpha
+ branch: main
name: refact-chat-js-latest
path: ./src/main/resources/webview/dist
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 183eebe5..eeec9686 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -4,6 +4,6 @@
-
+
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 94cb4e43..1e5fa47d 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -4,15 +4,17 @@ pluginGroup = "com.smallcloud"
pluginName = Refact.ai
pluginRepositoryUrl = https://github.com/smallcloudai/refact-intellij
# SemVer format -> https://semver.org
-pluginVersion = 5.1.0
+pluginVersion = 5.2.0
# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
-pluginSinceBuild = 232
+pluginSinceBuild = 233
pluginUntilBuild = 243.*
# IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension
platformType = PC
-platformVersion = 2023.2.8
+platformVersion = 2023.3.7
+#platformType = AI
+#platformVersion = 2023.3.1.2
# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
# Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP
diff --git a/refact_lsp b/refact_lsp
index a30852ff..000aadbe 100644
--- a/refact_lsp
+++ b/refact_lsp
@@ -1 +1 @@
-v0.10.2
\ No newline at end of file
+v0.10.3
\ No newline at end of file
diff --git a/src/main/kotlin/com/smallcloud/refactai/Initializer.kt b/src/main/kotlin/com/smallcloud/refactai/Initializer.kt
index d22804f2..f5480ec7 100644
--- a/src/main/kotlin/com/smallcloud/refactai/Initializer.kt
+++ b/src/main/kotlin/com/smallcloud/refactai/Initializer.kt
@@ -9,6 +9,7 @@ import com.intellij.openapi.startup.StartupActivity
import com.smallcloud.refactai.io.CloudMessageService
import com.smallcloud.refactai.listeners.UninstallListener
import com.smallcloud.refactai.lsp.LSPActiveDocNotifierService
+import com.smallcloud.refactai.lsp.LSPProcessHolder.Companion.initialize
import com.smallcloud.refactai.notifications.emitInfo
import com.smallcloud.refactai.notifications.notificationStartup
import com.smallcloud.refactai.panes.sharedchat.ChatPaneInvokeAction
@@ -25,6 +26,7 @@ class Initializer : StartupActivity, Disposable {
val shouldInitialize = !(initialized.getAndSet(true) || ApplicationManager.getApplication().isUnitTestMode)
if (shouldInitialize) {
Logger.getInstance("SMCInitializer").info("Bin prefix = ${Resources.binPrefix}")
+ initialize()
if (AppSettingsState.instance.isFirstStart) {
AppSettingsState.instance.isFirstStart = false
ChatPaneInvokeAction().actionPerformed()
diff --git a/src/main/kotlin/com/smallcloud/refactai/Resources.kt b/src/main/kotlin/com/smallcloud/refactai/Resources.kt
index 5a26c16b..bc23e020 100644
--- a/src/main/kotlin/com/smallcloud/refactai/Resources.kt
+++ b/src/main/kotlin/com/smallcloud/refactai/Resources.kt
@@ -89,7 +89,7 @@ object Resources {
val LOGO_RED_12x12: Icon = IconLoader.getIcon("/icons/refactai_logo_red_12x12.svg", Resources::class.java)
val LOGO_RED_13x13: Icon = IconLoader.getIcon("/icons/refactai_logo_red_13x13.svg", Resources::class.java)
- val LOGO_12x12: Icon = IconLoader.getIcon("/icons/refactai_logo_12x12.svg", Resources::class.java)
+ val LOGO_12x12: Icon = makeIcon("/icons/refactai_logo_12x12.svg")
val LOGO_RED_16x16: Icon = IconLoader.getIcon("/icons/refactai_logo_red_16x16.svg", Resources::class.java)
val COIN_16x16: Icon = makeIcon("/icons/coin_16x16.svg")
diff --git a/src/main/kotlin/com/smallcloud/refactai/code_lens/CodeLensAction.kt b/src/main/kotlin/com/smallcloud/refactai/code_lens/CodeLensAction.kt
new file mode 100644
index 00000000..52c58d0a
--- /dev/null
+++ b/src/main/kotlin/com/smallcloud/refactai/code_lens/CodeLensAction.kt
@@ -0,0 +1,71 @@
+package com.smallcloud.refactai.code_lens
+
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.editor.Editor
+import com.intellij.openapi.editor.LogicalPosition
+import com.intellij.openapi.project.DumbAwareAction
+import com.intellij.openapi.roots.ProjectRootManager
+import com.intellij.openapi.wm.ToolWindowManager
+import com.smallcloud.refactai.Resources
+import com.smallcloud.refactai.panes.RefactAIToolboxPaneFactory
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.io.path.relativeTo
+
+class CodeLensAction(
+ private val editor: Editor,
+ private val line1: Int,
+ private val line2: Int,
+ private val contentMsg: String,
+ private val sendImmediately: Boolean,
+ private val openNewTab: Boolean
+) : DumbAwareAction(Resources.Icons.LOGO_RED_16x16) {
+ override fun actionPerformed(p0: AnActionEvent) {
+ actionPerformed()
+ }
+
+ private fun formatMessage(): String {
+ val pos1 = LogicalPosition(line1, 0)
+ val text = editor.document.text.slice(
+ editor.logicalPositionToOffset(pos1) until editor.document.getLineEndOffset(line2)
+ )
+ val filePath = editor.virtualFile.toNioPath()
+ val relativePath = editor.project?.let {
+ ProjectRootManager.getInstance(it).contentRoots.map { root ->
+ filePath.relativeTo(root.toNioPath())
+ }.minBy { it.toString().length }
+ }
+
+ return contentMsg
+ .replace("%CURRENT_FILE%", relativePath?.toString() ?: filePath.toString())
+ .replace("%CURSOR_LINE%", line1.toString())
+ .replace("%CODE_SELECTION%", text)
+ }
+
+ private val isActionRunning = AtomicBoolean(false)
+
+ fun actionPerformed() {
+ val chat = editor.project?.let { ToolWindowManager.getInstance(it).getToolWindow("Refact") }
+
+ chat?.activate {
+ RefactAIToolboxPaneFactory.chat?.requestFocus()
+ RefactAIToolboxPaneFactory.chat?.executeCodeLensCommand(formatMessage(), sendImmediately, openNewTab)
+ }
+
+ // If content is empty, then it's "Open Chat" instruction, selecting range of code in active tab
+ if (contentMsg.isEmpty() && isActionRunning.compareAndSet(false, true)) {
+ ApplicationManager.getApplication().invokeLater {
+ try {
+ val pos1 = LogicalPosition(line1, 0)
+ val pos2 = LogicalPosition(line2, editor.document.getLineEndOffset(line2))
+
+ val intendedStart = editor.logicalPositionToOffset(pos1)
+ val intendedEnd = editor.logicalPositionToOffset(pos2)
+ editor.selectionModel.setSelection(intendedStart, intendedEnd)
+ } finally {
+ isActionRunning.set(false)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/smallcloud/refactai/code_lens/CodeLensInvalidatorService.kt b/src/main/kotlin/com/smallcloud/refactai/code_lens/CodeLensInvalidatorService.kt
new file mode 100644
index 00000000..290fab5c
--- /dev/null
+++ b/src/main/kotlin/com/smallcloud/refactai/code_lens/CodeLensInvalidatorService.kt
@@ -0,0 +1,30 @@
+package com.smallcloud.refactai.code_lens
+
+import com.intellij.codeInsight.codeVision.CodeVisionHost
+import com.intellij.codeInsight.codeVision.CodeVisionInitializer
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.application.invokeLater
+import com.intellij.openapi.components.service
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.project.Project
+import com.smallcloud.refactai.lsp.LSPProcessHolderChangedNotifier
+
+class CodeLensInvalidatorService(project: Project): Disposable {
+ private var ids: List = emptyList()
+ override fun dispose() {}
+ fun setCodeLensIds(ids: List) {
+ this.ids = ids
+ }
+
+ init {
+ project.messageBus.connect(this).subscribe(LSPProcessHolderChangedNotifier.TOPIC, object : LSPProcessHolderChangedNotifier {
+ override fun lspIsActive(isActive: Boolean) {
+ invokeLater {
+ logger().warn("Invalidating code lens")
+ project.service().getCodeVisionHost()
+ .invalidateProvider(CodeVisionHost.LensInvalidateSignal(null, ids))
+ }
+ }
+ })
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/smallcloud/refactai/code_lens/RefactCodeVisionProvider.kt b/src/main/kotlin/com/smallcloud/refactai/code_lens/RefactCodeVisionProvider.kt
new file mode 100644
index 00000000..999e5471
--- /dev/null
+++ b/src/main/kotlin/com/smallcloud/refactai/code_lens/RefactCodeVisionProvider.kt
@@ -0,0 +1,113 @@
+package com.smallcloud.refactai.code_lens
+
+import com.google.gson.Gson
+import com.google.gson.JsonObject
+import com.intellij.codeInsight.codeVision.*
+import com.intellij.codeInsight.codeVision.ui.model.ClickableTextCodeVisionEntry
+import com.intellij.openapi.application.runReadAction
+import com.intellij.openapi.diagnostic.Logger
+import com.intellij.openapi.editor.Editor
+import com.intellij.openapi.editor.LogicalPosition
+import com.intellij.openapi.util.TextRange
+import com.smallcloud.refactai.Resources
+import com.smallcloud.refactai.lsp.LSPProcessHolder.Companion.getInstance
+import com.smallcloud.refactai.lsp.lspGetCodeLens
+import com.smallcloud.refactai.struct.ChatMessage
+import kotlin.math.max
+import com.intellij.codeInsight.codeVision.CodeVisionBundle
+data class CodeLen(
+ val range: TextRange,
+ val label: String,
+ val action: CodeLensAction
+)
+
+fun makeIdForProvider(commandKey: String): String {
+ return "refactai.codelens.$commandKey"
+}
+
+class RefactCodeVisionProvider(
+ private val commandKey: String,
+ private val posAfter: String?,
+ private val label: String,
+ private val customization: JsonObject
+) :
+ CodeVisionProvider {
+ override val defaultAnchor: CodeVisionAnchorKind
+ get() = CodeVisionAnchorKind.Top
+ override val id: String
+ get() = makeIdForProvider(commandKey)
+ override val name: String
+ get() = "Refact.ai Hint($label)"
+ override val relativeOrderings: List
+ get() {
+ return if (posAfter == null) {
+ listOf(CodeVisionRelativeOrdering.CodeVisionRelativeOrderingFirst)
+ } else {
+ listOf(CodeVisionRelativeOrdering.CodeVisionRelativeOrderingAfter("refactai.codelens.$posAfter"))
+ }
+ }
+
+ override fun precomputeOnUiThread(editor: Editor) {}
+
+ private fun getCodeLens(editor: Editor): List {
+ val codeLensStr = lspGetCodeLens(editor)
+ val gson = Gson()
+ val codeLensJson = gson.fromJson(codeLensStr, JsonObject::class.java)
+ val resCodeLenses = mutableListOf()
+ if (customization.has("code_lens")) {
+ val allCodeLenses = customization.get("code_lens").asJsonObject
+ if (codeLensJson.has("code_lens")) {
+ val codeLenses = codeLensJson.get("code_lens")!!.asJsonArray
+ for (codeLens in codeLenses) {
+ val line1 = max(codeLens.asJsonObject.get("line1").asInt - 1, 0)
+ val line2 = max(codeLens.asJsonObject.get("line2").asInt - 1, 0)
+ val range = runReadAction {
+ return@runReadAction TextRange(
+ editor.logicalPositionToOffset(LogicalPosition(line1, 0)),
+ editor.document.getLineEndOffset(line2)
+ )
+ }
+ val value = allCodeLenses.get(commandKey).asJsonObject
+ val msgs = value.asJsonObject.get("messages").asJsonArray.map {
+ gson.fromJson(it.asJsonObject, ChatMessage::class.java)
+ }.toList()
+ val msg = msgs.find { it.role == "user" }
+ val sendImmediately = value.asJsonObject.get("auto_submit").asBoolean
+ val openNewTab = value.asJsonObject.get("new_tab")?.asBoolean ?: true
+ if (msg != null || msgs.isEmpty()) {
+ resCodeLenses.add(
+ CodeLen(
+ range,
+ value.asJsonObject.get("label").asString,
+ CodeLensAction(editor, line1, line2, msg?.content ?: "", sendImmediately, openNewTab)
+ )
+ )
+ }
+ }
+ }
+ }
+
+ return resCodeLenses
+ }
+
+ override fun computeCodeVision(editor: Editor, uiData: Unit): CodeVisionState {
+ Logger.getInstance(RefactCodeVisionProvider::class.java).warn("computeCodeVision $commandKey start")
+ val lsp = editor.project?.let { getInstance(it) } ?: return CodeVisionState.NotReady
+ if (!lsp.isWorking) return CodeVisionState.NotReady
+
+ try {
+ val codeLens = getCodeLens(editor)
+ val result = ArrayList>()
+ Logger.getInstance(RefactCodeVisionProvider::class.java)
+ .warn("computeCodeVision $commandKey ${codeLens.size}")
+ for (codeLen in codeLens) {
+ result.add(codeLen.range to ClickableTextCodeVisionEntry(codeLen.label, id, { event, editor ->
+ codeLen.action.actionPerformed()
+ }, Resources.Icons.LOGO_12x12))
+ }
+ return CodeVisionState.Ready(result)
+ } catch (e: Exception) {
+ return CodeVisionState.NotReady
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/smallcloud/refactai/code_lens/RefactCodeVisionProviderFactory.kt b/src/main/kotlin/com/smallcloud/refactai/code_lens/RefactCodeVisionProviderFactory.kt
new file mode 100644
index 00000000..01c95052
--- /dev/null
+++ b/src/main/kotlin/com/smallcloud/refactai/code_lens/RefactCodeVisionProviderFactory.kt
@@ -0,0 +1,58 @@
+package com.smallcloud.refactai.code_lens
+
+import com.intellij.codeInsight.codeVision.CodeVisionProvider
+import com.intellij.codeInsight.codeVision.CodeVisionProviderFactory
+import com.intellij.codeInsight.codeVision.settings.CodeVisionGroupSettingProvider
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+import com.smallcloud.refactai.RefactAIBundle
+import com.smallcloud.refactai.lsp.LSPProcessHolder.Companion.initialize
+import com.smallcloud.refactai.lsp.LSPProcessHolder.Companion.getInstance as getLSPProcessHolder
+
+// hardcode default codelens from lsp customization
+class RefactOpenChatSettingProvider : CodeVisionGroupSettingProvider {
+ override val groupId: String
+ get() = makeIdForProvider("open_chat")
+ override val groupName: String
+ get() = RefactAIBundle.message("codeVision.openChat.name")
+}
+
+class RefactOpenProblemsSettingProvider : CodeVisionGroupSettingProvider {
+ override val groupId: String
+ get() = makeIdForProvider("problems")
+ override val groupName: String
+ get() = RefactAIBundle.message("codeVision.problems.name")
+}
+
+class RefactOpenExplainSettingProvider : CodeVisionGroupSettingProvider {
+ override val groupId: String
+ get() = makeIdForProvider("explain")
+ override val groupName: String
+ get() = RefactAIBundle.message("codeVision.explain.name")
+}
+
+class RefactCodeVisionProviderFactory : CodeVisionProviderFactory {
+ override fun createProviders(project: Project): Sequence> {
+ initialize()
+ val customization = getLSPProcessHolder(project)?.fetchCustomization() ?: return emptySequence()
+ if (customization.has("code_lens")) {
+ val allCodeLenses = customization.get("code_lens").asJsonObject
+ val allCodeLensKeys = allCodeLenses.keySet().toList()
+ val providers: MutableList> = mutableListOf()
+ for ((idx, key) in allCodeLensKeys.withIndex()) {
+ val label = allCodeLenses.get(key).asJsonObject.get("label").asString
+ var posAfter: String? = null
+ if (idx != 0) {
+ posAfter = allCodeLensKeys[idx - 1]
+ }
+ providers.add(RefactCodeVisionProvider(key, posAfter, label, customization))
+ }
+ val ids = providers.map { it.id }
+ project.service().setCodeLensIds(ids)
+
+ return providers.asSequence()
+
+ }
+ return emptySequence()
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/smallcloud/refactai/listeners/LastEditorGetterListener.kt b/src/main/kotlin/com/smallcloud/refactai/listeners/LastEditorGetterListener.kt
index 05b6c06d..32c1a6bf 100644
--- a/src/main/kotlin/com/smallcloud/refactai/listeners/LastEditorGetterListener.kt
+++ b/src/main/kotlin/com/smallcloud/refactai/listeners/LastEditorGetterListener.kt
@@ -5,21 +5,17 @@ import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.editor.event.EditorFactoryEvent
import com.intellij.openapi.editor.event.EditorFactoryListener
-import com.intellij.openapi.editor.event.SelectionEvent
-import com.intellij.openapi.editor.event.SelectionListener
+import com.intellij.openapi.editor.ex.EditorEx
+import com.intellij.openapi.editor.ex.FocusChangeListener
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.fileEditor.FileEditorManager
-import com.intellij.openapi.fileEditor.FileEditorManagerEvent
import com.intellij.openapi.fileEditor.FileEditorManagerListener
import com.intellij.openapi.vfs.VirtualFile
-import com.intellij.util.concurrency.AppExecutorUtil
import com.intellij.util.messages.Topic
import com.smallcloud.refactai.PluginState
-import java.util.concurrent.ScheduledFuture
interface SelectionChangedNotifier {
- fun isSelectionChanged(isSelection: Boolean) {}
fun isEditorChanged(editor: Editor?) {}
companion object {
@@ -27,49 +23,37 @@ interface SelectionChangedNotifier {
}
}
-class GlobalSelectionListener : SelectionListener {
- private var lastTask: ScheduledFuture<*>? = null
- override fun selectionChanged(e: SelectionEvent) {
- val isSelection = e.newRange.length > 0
- lastTask?.cancel(true)
- lastTask = AppExecutorUtil.getAppScheduledExecutorService().schedule({
- ApplicationManager.getApplication().messageBus
- .syncPublisher(SelectionChangedNotifier.TOPIC)
- .isSelectionChanged(isSelection)
-
- }, 500, java.util.concurrent.TimeUnit.MILLISECONDS)
- }
-}
-
class LastEditorGetterListener : EditorFactoryListener, FileEditorManagerListener {
- private val selectorListener = GlobalSelectionListener()
+ private val focusChangeListener = object : FocusChangeListener {
+ override fun focusGained(editor: Editor) {
+ setEditor(editor)
+ }
+ }
init {
ApplicationManager.getApplication()
- .messageBus.connect(PluginState.instance)
- .subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, this)
+ .messageBus.connect(PluginState.instance)
+ .subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, this)
instance = this
}
+ private fun setEditor(editor: Editor) {
+ if (LAST_EDITOR != editor) {
+ LAST_EDITOR = editor
+ ApplicationManager.getApplication().messageBus
+ .syncPublisher(SelectionChangedNotifier.TOPIC)
+ .isEditorChanged(editor)
+ }
+ }
+
private fun setup(editor: Editor) {
- LAST_EDITOR = editor
- LAST_EDITOR!!.selectionModel.addSelectionListener(selectorListener, PluginState.instance)
+ (editor as EditorEx).addFocusListener(focusChangeListener)
}
private fun getVirtualFile(editor: Editor): VirtualFile? {
return FileDocumentManager.getInstance().getFile(editor.document)
}
- override fun selectionChanged(event: FileEditorManagerEvent) {
- val editor = event.newFile?.let {
- EditorFactory.getInstance().allEditors.firstOrNull { getVirtualFile(it) == event.newFile }
- }
- LAST_EDITOR = editor
- ApplicationManager.getApplication().messageBus
- .syncPublisher(SelectionChangedNotifier.TOPIC)
- .isEditorChanged(LAST_EDITOR)
- }
-
override fun fileOpened(source: FileEditorManager, file: VirtualFile) {
val editor = EditorFactory.getInstance().allEditors.firstOrNull { getVirtualFile(it) == file }
if (editor != null) {
diff --git a/src/main/kotlin/com/smallcloud/refactai/lsp/LSPActiveDocNotifierService.kt b/src/main/kotlin/com/smallcloud/refactai/lsp/LSPActiveDocNotifierService.kt
index 90bd9d37..bd677e12 100644
--- a/src/main/kotlin/com/smallcloud/refactai/lsp/LSPActiveDocNotifierService.kt
+++ b/src/main/kotlin/com/smallcloud/refactai/lsp/LSPActiveDocNotifierService.kt
@@ -2,12 +2,17 @@ package com.smallcloud.refactai.lsp
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
-import com.intellij.openapi.project.Project
import com.intellij.openapi.editor.Editor
+import com.intellij.openapi.project.Project
+import com.smallcloud.refactai.listeners.LastEditorGetterListener
import com.smallcloud.refactai.listeners.SelectionChangedNotifier
class LSPActiveDocNotifierService(val project: Project): Disposable {
init {
+ if (LastEditorGetterListener.LAST_EDITOR != null) {
+ lspSetActiveDocument(LastEditorGetterListener.LAST_EDITOR!!)
+ }
+
ApplicationManager.getApplication().messageBus.connect(this)
.subscribe(SelectionChangedNotifier.TOPIC, object : SelectionChangedNotifier {
override fun isEditorChanged(editor: Editor?) {
diff --git a/src/main/kotlin/com/smallcloud/refactai/lsp/LSPHelper.kt b/src/main/kotlin/com/smallcloud/refactai/lsp/LSPHelper.kt
index 47d55540..3ce10850 100644
--- a/src/main/kotlin/com/smallcloud/refactai/lsp/LSPHelper.kt
+++ b/src/main/kotlin/com/smallcloud/refactai/lsp/LSPHelper.kt
@@ -1,11 +1,11 @@
package com.smallcloud.refactai.lsp
import com.google.gson.Gson
+import com.intellij.openapi.editor.Editor
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.vfs.VirtualFile
-import com.intellij.openapi.editor.Editor
import com.smallcloud.refactai.io.ConnectionStatus
import com.smallcloud.refactai.io.InferenceGlobalContext.Companion.instance as InferenceGlobalContext
import com.smallcloud.refactai.lsp.LSPProcessHolder.Companion.getInstance as getLSPProcessHolder
@@ -31,7 +31,7 @@ fun lspProjectInitialize(lsp: LSPProcessHolder, project: Project) {
}
fun lspDocumentDidChanged(project: Project, docUrl: String, text: String) {
- val url = getLSPProcessHolder(project).url.resolve("/v1/lsp-did-changed")
+ val url = getLSPProcessHolder(project)?.url?.resolve("/v1/lsp-did-changed") ?: return
val data = Gson().toJson(
mapOf(
"uri" to docUrl,
@@ -59,7 +59,7 @@ fun lspSetActiveDocument(editor: Editor) {
val vFile = getVirtualFile(editor) ?: return
if (!vFile.exists()) return
- val url = getLSPProcessHolder(project).url.resolve("/v1/lsp-set-active-document")
+ val url = getLSPProcessHolder(project)?.url?.resolve("/v1/lsp-set-active-document") ?: return
val data = Gson().toJson(
mapOf(
"uri" to vFile.url,
@@ -75,4 +75,28 @@ fun lspSetActiveDocument(editor: Editor) {
InferenceGlobalContext.lastErrorMsg = it.message
}
})
-}
\ No newline at end of file
+}
+
+
+fun lspGetCodeLens(editor: Editor): String {
+ val project = editor.project!!
+ val url = getLSPProcessHolder(project)?.url?.resolve("/v1/code-lens") ?: return ""
+ val data = Gson().toJson(
+ mapOf(
+ "uri" to editor.virtualFile.url,
+ )
+ )
+
+ InferenceGlobalContext.connection.post(url, data, dataReceiveEnded={
+ InferenceGlobalContext.status = ConnectionStatus.CONNECTED
+ InferenceGlobalContext.lastErrorMsg = null
+ }, failedDataReceiveEnded = {
+ InferenceGlobalContext.status = ConnectionStatus.ERROR
+ if (it != null) {
+ InferenceGlobalContext.lastErrorMsg = it.message
+ }
+ }).let {
+ val res = it.get()!!.get() as String
+ return res
+ }
+}
diff --git a/src/main/kotlin/com/smallcloud/refactai/lsp/LSPProcessHolder.kt b/src/main/kotlin/com/smallcloud/refactai/lsp/LSPProcessHolder.kt
index 6079b01d..83a4951a 100644
--- a/src/main/kotlin/com/smallcloud/refactai/lsp/LSPProcessHolder.kt
+++ b/src/main/kotlin/com/smallcloud/refactai/lsp/LSPProcessHolder.kt
@@ -1,6 +1,7 @@
package com.smallcloud.refactai.lsp
import com.google.gson.Gson
+import com.google.gson.JsonObject
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationInfo
@@ -21,12 +22,16 @@ import com.smallcloud.refactai.io.ConnectionStatus
import com.smallcloud.refactai.io.InferenceGlobalContextChangedNotifier
import com.smallcloud.refactai.notifications.emitError
import org.apache.hc.core5.concurrent.ComplexFuture
+import java.io.File
+import java.io.FileOutputStream
+import java.io.InputStream
import java.net.URI
-import java.nio.file.Files
import java.nio.file.Paths
-import java.nio.file.StandardCopyOption
+import java.security.MessageDigest
+import java.util.*
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
import kotlin.io.path.Path
import com.smallcloud.refactai.account.AccountManager.Companion.instance as AccountManager
import com.smallcloud.refactai.io.InferenceGlobalContext.Companion.instance as InferenceGlobalContext
@@ -43,8 +48,7 @@ interface LSPProcessHolderChangedNotifier {
companion object {
val TOPIC = Topic.create(
- "Connection Changed Notifier",
- LSPProcessHolderChangedNotifier::class.java
+ "Connection Changed Notifier", LSPProcessHolderChangedNotifier::class.java
)
}
}
@@ -52,7 +56,6 @@ interface LSPProcessHolderChangedNotifier {
class LSPProcessHolder(val project: Project) : Disposable {
private var process: Process? = null
private var lastConfig: LSPConfig? = null
- private val logger = Logger.getInstance("LSPProcessHolder")
private val loggerScheduler = AppExecutorUtil.createBoundedScheduledExecutorService(
"SMCLSPLoggerScheduler", 1
)
@@ -66,37 +69,36 @@ class LSPProcessHolder(val project: Project) : Disposable {
"SMCLSHealthCheckerScheduler", 1
)
+ private val exitThread: Thread = Thread {
+ terminate()
+ }
+
var isWorking: Boolean
get() = isWorking_
set(newValue) {
if (isWorking_ == newValue) return
if (!project.isDisposed) {
- project
- .messageBus
- .syncPublisher(LSPProcessHolderChangedNotifier.TOPIC)
- .lspIsActive(newValue)
+ project.messageBus.syncPublisher(LSPProcessHolderChangedNotifier.TOPIC).lspIsActive(newValue)
}
isWorking_ = newValue
}
init {
- messageBus
- .connect(this)
- .subscribe(AccountManagerChangedNotifier.TOPIC, object : AccountManagerChangedNotifier {
- override fun apiKeyChanged(newApiKey: String?) {
- AppExecutorUtil.getAppScheduledExecutorService().submit {
- settingsChanged()
- }
+ initialize()
+ messageBus.connect(this).subscribe(AccountManagerChangedNotifier.TOPIC, object : AccountManagerChangedNotifier {
+ override fun apiKeyChanged(newApiKey: String?) {
+ AppExecutorUtil.getAppScheduledExecutorService().submit {
+ settingsChanged()
}
+ }
- override fun planStatusChanged(newPlan: String?) {
- AppExecutorUtil.getAppScheduledExecutorService().submit {
- settingsChanged()
- }
+ override fun planStatusChanged(newPlan: String?) {
+ AppExecutorUtil.getAppScheduledExecutorService().submit {
+ settingsChanged()
}
- })
- messageBus
- .connect(this)
+ }
+ })
+ messageBus.connect(this)
.subscribe(InferenceGlobalContextChangedNotifier.TOPIC, object : InferenceGlobalContextChangedNotifier {
override fun userInferenceUriChanged(newUrl: String?) {
AppExecutorUtil.getAppScheduledExecutorService().submit {
@@ -147,25 +149,7 @@ class LSPProcessHolder(val project: Project) : Disposable {
}
})
- Companion::class.java.getResourceAsStream(
- "/bin/${binPrefix}/refact-lsp${getExeSuffix()}"
- ).use { input ->
- if (input == null) {
- emitError("LSP server is not found for host operating system, please contact support")
- } else {
- for (i in 0..4) {
- try {
- val path = Paths.get(BIN_PATH)
- path.parent.toFile().mkdirs()
- Files.copy(input, path, StandardCopyOption.REPLACE_EXISTING)
- setExecutable(path.toFile())
- break
- } catch (e: Exception) {
- logger.warn(e.message)
- }
- }
- }
- }
+ Runtime.getRuntime().addShutdownHook(exitThread)
settingsChanged()
healthCheckerScheduler.scheduleWithFixedDelay({
@@ -177,12 +161,12 @@ class LSPProcessHolder(val project: Project) : Disposable {
}, 1, 1, TimeUnit.SECONDS)
}
-
private fun settingsChanged() {
synchronized(this) {
terminate()
if (InferenceGlobalContext.xDebugLSPPort != null) {
capabilities = getCaps()
+ isWorking = true
lspProjectInitialize(this, project)
return
}
@@ -194,15 +178,11 @@ class LSPProcessHolder(val project: Project) : Disposable {
set(newValue) {
if (newValue == field) return
field = newValue
- project
- .messageBus
- .syncPublisher(LSPProcessHolderChangedNotifier.TOPIC)
- .capabilitiesChanged(field)
+ project.messageBus.syncPublisher(LSPProcessHolderChangedNotifier.TOPIC).capabilitiesChanged(field)
}
private fun startProcess() {
- val address = if (InferenceGlobalContext.inferenceUri == null) "Refact" else
- InferenceGlobalContext.inferenceUri
+ val address = if (InferenceGlobalContext.inferenceUri == null) "Refact" else InferenceGlobalContext.inferenceUri
val newConfig = LSPConfig(
address = address,
apiKey = AccountManager.apiKey,
@@ -230,8 +210,7 @@ class LSPProcessHolder(val project: Project) : Disposable {
try {
newConfig.port = (32000..32199).random()
logger.warn("LSP start_process " + BIN_PATH + " " + newConfig.toArgs())
- process = GeneralCommandLine(listOf(BIN_PATH) + newConfig.toArgs())
- .withRedirectErrorStream(true)
+ process = GeneralCommandLine(listOf(BIN_PATH) + newConfig.toArgs()).withRedirectErrorStream(true)
.createProcess()
process!!.waitFor(5, TimeUnit.SECONDS)
lastConfig = newConfig
@@ -254,8 +233,7 @@ class LSPProcessHolder(val project: Project) : Disposable {
}
process!!.onExit().thenAcceptAsync { process1 ->
if (process1.exitValue() != 0) {
- logger.warn("LSP bad_things_happened " +
- process1.inputStream.bufferedReader().use { it.readText() })
+ logger.warn("LSP bad_things_happened " + process1.inputStream.bufferedReader().use { it.readText() })
}
}
attempt = 0
@@ -263,8 +241,9 @@ class LSPProcessHolder(val project: Project) : Disposable {
try {
InferenceGlobalContext.connection.ping(url)
buildInfo = getBuildInfo()
+ logger.warn("LSP binary build info $buildInfo")
capabilities = getCaps()
- fetchToolboxConfig()
+ fetchCustomization()
isWorking = true
break
} catch (e: Exception) {
@@ -276,20 +255,19 @@ class LSPProcessHolder(val project: Project) : Disposable {
lspProjectInitialize(this, project)
}
- private fun fetchToolboxConfig(): String {
- val config = InferenceGlobalContext.connection.get(url.resolve("/v1/customization"),
- dataReceiveEnded={
- InferenceGlobalContext.status = ConnectionStatus.CONNECTED
- InferenceGlobalContext.lastErrorMsg = null
- },
- errorDataReceived = {},
- failedDataReceiveEnded = {
- InferenceGlobalContext.status = ConnectionStatus.ERROR
- if (it != null) {
- InferenceGlobalContext.lastErrorMsg = it.message
- }
- }).join().get()
- return config as String
+ fun fetchCustomization(): JsonObject? {
+ if (!isWorking) return getCustomizationDirectly()
+
+ val config = InferenceGlobalContext.connection.get(url.resolve("/v1/customization"), dataReceiveEnded = {
+ InferenceGlobalContext.status = ConnectionStatus.CONNECTED
+ InferenceGlobalContext.lastErrorMsg = null
+ }, errorDataReceived = {}, failedDataReceiveEnded = {
+ InferenceGlobalContext.status = ConnectionStatus.ERROR
+ if (it != null) {
+ InferenceGlobalContext.lastErrorMsg = it.message
+ }
+ }).join().get()
+ return Gson().fromJson(config as String, JsonObject::class.java)
}
private fun safeTerminate() {
@@ -301,9 +279,9 @@ class LSPProcessHolder(val project: Project) : Disposable {
}
private fun terminate() {
+ isWorking = false
process?.let {
try {
- isWorking = false
safeTerminate()
if (it.waitFor(3, TimeUnit.SECONDS)) {
logger.info("LSP SIGTERM")
@@ -315,46 +293,30 @@ class LSPProcessHolder(val project: Project) : Disposable {
}
}
- companion object {
- val BIN_PATH = Path(
- getTempDirectory(),
- ApplicationInfo.getInstance().build.toString().replace(Regex("[^A-Za-z0-9 ]"), "_") +
- "_refact_lsp${getExeSuffix()}"
- ).toString()
-
- // here ?
- @JvmStatic
- fun getInstance(project: Project): LSPProcessHolder = project.service()
-
- var buildInfo: String = ""
- }
-
override fun dispose() {
terminate()
loggerScheduler.shutdown()
schedulerCaps.shutdown()
healthCheckerScheduler.shutdown()
+ Runtime.getRuntime().removeShutdownHook(exitThread)
}
private fun getBuildInfo(): String {
var res = ""
- InferenceGlobalContext.connection.get(url.resolve("/build_info"),
- dataReceiveEnded={
- InferenceGlobalContext.status = ConnectionStatus.CONNECTED
- InferenceGlobalContext.lastErrorMsg = null
- },
- errorDataReceived = {},
- failedDataReceiveEnded = {
- InferenceGlobalContext.status = ConnectionStatus.ERROR
- if (it != null) {
- InferenceGlobalContext.lastErrorMsg = it.message
- }
- }).also {
+ InferenceGlobalContext.connection.get(url.resolve("/build_info"), dataReceiveEnded = {
+ InferenceGlobalContext.status = ConnectionStatus.CONNECTED
+ InferenceGlobalContext.lastErrorMsg = null
+ }, errorDataReceived = {}, failedDataReceiveEnded = {
+ InferenceGlobalContext.status = ConnectionStatus.ERROR
+ if (it != null) {
+ InferenceGlobalContext.lastErrorMsg = it.message
+ }
+ }).also {
try {
res = it.get().get() as String
- logger.debug("build_info request finished")
+ logger.warn("build_info request finished")
} catch (e: Exception) {
- logger.debug("build_info ${e.message}")
+ logger.warn("build_info ${e.message}")
}
}
return res
@@ -369,18 +331,14 @@ class LSPProcessHolder(val project: Project) : Disposable {
fun getCaps(): LSPCapabilities {
var res = LSPCapabilities()
- InferenceGlobalContext.connection.get(url.resolve("/v1/caps"),
- dataReceiveEnded={
- InferenceGlobalContext.status = ConnectionStatus.CONNECTED
- InferenceGlobalContext.lastErrorMsg = null
- },
- errorDataReceived = {},
- failedDataReceiveEnded = {
- if (it != null) {
- InferenceGlobalContext.lastErrorMsg = it.message
- }
+ InferenceGlobalContext.connection.get(url.resolve("/v1/caps"), dataReceiveEnded = {
+ InferenceGlobalContext.status = ConnectionStatus.CONNECTED
+ InferenceGlobalContext.lastErrorMsg = null
+ }, errorDataReceived = {}, failedDataReceiveEnded = {
+ if (it != null) {
+ InferenceGlobalContext.lastErrorMsg = it.message
}
- ).also {
+ }).also {
val requestFuture: ComplexFuture<*>?
try {
requestFuture = it.get() as ComplexFuture
@@ -397,10 +355,9 @@ class LSPProcessHolder(val project: Project) : Disposable {
}
fun getRagStatus(): RagStatus? {
- InferenceGlobalContext.connection.get(
- url.resolve("/v1/rag-status"),
+ InferenceGlobalContext.connection.get(url.resolve("/v1/rag-status"),
requestProperties = mapOf("redirect" to "follow", "cache" to "no-cache", "referrer" to "no-referrer"),
- dataReceiveEnded={
+ dataReceiveEnded = {
InferenceGlobalContext.status = ConnectionStatus.CONNECTED
InferenceGlobalContext.lastErrorMsg = null
},
@@ -436,4 +393,108 @@ class LSPProcessHolder(val project: Project) : Disposable {
return ""
}
}
+
+ companion object {
+ var BIN_PATH: String? = null
+ private var TMP_BIN_PATH: String? = null
+
+ @JvmStatic
+ fun getInstance(project: Project): LSPProcessHolder? = project.service()
+
+ var buildInfo: String = ""
+ private val initialized = AtomicBoolean(false)
+ private val logger = Logger.getInstance("LSPProcessHolder")
+
+ private fun generateMD5HexAndWriteInTmpFile(input: InputStream, tmpFileName: File): String {
+ val digest = MessageDigest.getInstance("MD5")
+ val buffer = ByteArray(1024)
+ var bytesRead: Int
+ val fileOut = FileOutputStream(tmpFileName)
+ while (input.read(buffer).also { bytesRead = it } != -1) {
+ digest.update(buffer, 0, bytesRead)
+ fileOut.write(buffer, 0, bytesRead)
+ }
+ fileOut.flush()
+ fileOut.close()
+ input.close()
+ return digest.digest().joinToString("") { String.format("%02x", it) }
+ }
+
+ // only one time
+ fun initialize() {
+ val shouldInitialize = !initialized.getAndSet(true)
+ if (!shouldInitialize) return
+
+ Companion::class.java.getResourceAsStream(
+ "/bin/${binPrefix}/refact-lsp${getExeSuffix()}"
+ ).use { input ->
+ if (input == null) {
+ emitError("LSP server is not found for host operating system, please contact support")
+ } else {
+ val tmpFileName =
+ Path(getTempDirectory(), "${UUID.randomUUID().toString()}${getExeSuffix()}").toFile()
+ TMP_BIN_PATH = tmpFileName.toString()
+ val hash = generateMD5HexAndWriteInTmpFile(input, tmpFileName)
+ BIN_PATH = Path(
+ getTempDirectory(),
+ ApplicationInfo.getInstance().build.toString()
+ .replace(Regex("[^A-Za-z0-9 ]"), "_") + "_refact_lsp_${hash}${getExeSuffix()}"
+ ).toString()
+ var shouldUseTmp = false
+ for (i in 0..4) {
+ try {
+ val path = Paths.get(BIN_PATH!!)
+ path.parent.toFile().mkdirs()
+ if (tmpFileName.renameTo(path.toFile())) {
+ setExecutable(path.toFile())
+ }
+ shouldUseTmp = false
+ break
+ } catch (e: Exception) {
+ logger.warn("LSP bad_things_happened: can not save binary $BIN_PATH")
+ logger.warn("LSP bad_things_happened: error message - ${e.message}")
+ shouldUseTmp = true
+ }
+ }
+ if (shouldUseTmp) {
+ setExecutable(tmpFileName)
+ BIN_PATH = TMP_BIN_PATH
+ } else {
+ if (tmpFileName.exists()) {
+ tmpFileName.deleteOnExit()
+ }
+ }
+ }
+ }
+ }
+
+ // run after close application
+ fun cleanup() {
+
+ }
+
+ fun getCustomizationDirectly(): JsonObject? {
+ val process = GeneralCommandLine(listOf(BIN_PATH, "--print-customization")).withRedirectErrorStream(true)
+ .createProcess()
+ val isExit = process.waitFor(3, TimeUnit.SECONDS)
+ if (isExit) {
+ if (process.exitValue() != 0) {
+ logger.warn("LSP bad_things_happened " + process.inputStream.bufferedReader().use { it.readText() })
+ return null
+ }
+ } else {
+ process.destroy() // win11 doesn't finish process safe
+ }
+
+ val out = process.inputStream.bufferedReader().use { it.readText() }
+ val customizationStr = out.trim().lines().last()
+ return try {
+ Gson().fromJson(customizationStr, JsonObject::class.java)
+ } catch (e: Exception) {
+ logger.warn("LSP can not parse json string $customizationStr")
+ logger.warn("LSP can not parse json string error = ${e.message}")
+ null
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/src/main/kotlin/com/smallcloud/refactai/modes/completion/CompletionMode.kt b/src/main/kotlin/com/smallcloud/refactai/modes/completion/CompletionMode.kt
index d3e79e80..2f878070 100644
--- a/src/main/kotlin/com/smallcloud/refactai/modes/completion/CompletionMode.kt
+++ b/src/main/kotlin/com/smallcloud/refactai/modes/completion/CompletionMode.kt
@@ -121,7 +121,7 @@ class CompletionMode(
val promptInfo: List = listOf()
val stat = UsageStatistic(scope, extension = getExtension(fileName))
- val baseUrl = getLSPProcessHolder(project).url
+ val baseUrl = getLSPProcessHolder(project)?.url!!
val request = RequestCreator.create(
fileName, text, logicalPos.line, pos,
stat, "Infill", "infill", promptInfo,
diff --git a/src/main/kotlin/com/smallcloud/refactai/modes/diff/DiffLayout.kt b/src/main/kotlin/com/smallcloud/refactai/modes/diff/DiffLayout.kt
index d752327b..b758aca2 100644
--- a/src/main/kotlin/com/smallcloud/refactai/modes/diff/DiffLayout.kt
+++ b/src/main/kotlin/com/smallcloud/refactai/modes/diff/DiffLayout.kt
@@ -67,7 +67,7 @@ class DiffLayout(
DeltaType.INSERT -> {
document.insertString(
getOffsetFromStringNumber(det.source.position),
- det.target.lines!!.joinToString("\n", postfix = "\n")
+ det.target.lines!!.joinToString("")
)
}
@@ -78,7 +78,7 @@ class DiffLayout(
)
document.insertString(
getOffsetFromStringNumber(det.source.position),
- det.target.lines!!.joinToString("\n", postfix = "\n")
+ det.target.lines!!.joinToString("")
)
}
diff --git a/src/main/kotlin/com/smallcloud/refactai/modes/diff/DiffMode.kt b/src/main/kotlin/com/smallcloud/refactai/modes/diff/DiffMode.kt
index 99bdba75..35bac75f 100644
--- a/src/main/kotlin/com/smallcloud/refactai/modes/diff/DiffMode.kt
+++ b/src/main/kotlin/com/smallcloud/refactai/modes/diff/DiffMode.kt
@@ -70,7 +70,6 @@ class DiffMode(
editor: Editor,
content: String
) {
-
val selectionModel = editor.selectionModel
val startSelectionOffset: Int = selectionModel.selectionStart
val endSelectionOffset: Int = selectionModel.selectionEnd
@@ -86,12 +85,12 @@ class DiffMode(
val diff = DiffLayout(editor, content)
val originalText = editor.document.text
val newText = originalText.replaceRange(startSelectionOffset, endSelectionOffset, indentedCode)
- val patch = DiffUtils.diff(originalText.split("\n"), newText.split("\n"))
+ val patch = DiffUtils.diff(originalText.split("(?<=\n)".toRegex()), newText.split("(?<=\n)".toRegex()))
diffLayout = diff.update(patch)
app.invokeLater {
- editor.contentComponent.requestFocus()
+ editor.contentComponent.requestFocusInWindow()
}
}
}
\ No newline at end of file
diff --git a/src/main/kotlin/com/smallcloud/refactai/modes/diff/renderer/Inlayer.kt b/src/main/kotlin/com/smallcloud/refactai/modes/diff/renderer/Inlayer.kt
index f021932d..fbcd32ee 100644
--- a/src/main/kotlin/com/smallcloud/refactai/modes/diff/renderer/Inlayer.kt
+++ b/src/main/kotlin/com/smallcloud/refactai/modes/diff/renderer/Inlayer.kt
@@ -37,13 +37,29 @@ class Inlayer(val editor: Editor, private val intent: String) : Disposable {
rangeHighlighters.clear()
}
- private fun renderInsertBlock(lines: List, smallPatches: List>, offset: Int) {
- val logicalPosition = editor.offsetToLogicalPosition(offset)
+ private fun getPriorityForOffset(offset: Int, isAbove: Boolean): Int {
+ val allInlaysForOffset = inlays.filter {
+ it.offset == offset
+ }.sortedBy { it.properties.priority }
+ return if (isAbove) {
+ (allInlaysForOffset.lastOrNull()?.properties?.priority?.plus(1)) ?: 1
+ } else {
+ (allInlaysForOffset.firstOrNull()?.properties?.priority?.minus(1)) ?: 998
+ }
+ }
+
+ private fun renderInsertBlock(lines: List, smallPatches: List>,
+ offset: Int, targetLine: Int, isInline: Boolean = false) {
val renderer = InsertBlockElementRenderer(editor, lines, smallPatches, false)
- val isAbove = (logicalPosition.line < 1)
- val element = editor
- .inlayModel
- .addBlockElement(offset, false, isAbove, if (isAbove) 1 else 998, renderer)
+ val isAbove = (targetLine < 1)
+ val priority = getPriorityForOffset(offset, isAbove)
+ val element = if (isInline) {
+ editor.inlayModel.addInlineElement(offset, false, renderer)
+ } else {
+ editor
+ .inlayModel
+ .addBlockElement(offset, false, isAbove, priority, renderer)
+ }
element?.let {
Disposer.register(this, it)
inlays.add(element)
@@ -99,9 +115,13 @@ class Inlayer(val editor: Editor, private val intent: String) : Disposable {
if (det.target.lines == null) continue
when (det.type) {
DeltaType.INSERT -> {
+ val isInline = det.source.lines?.let {
+ it.size == 1 && it.first().isEmpty()
+ } ?: false
renderInsertBlock(
det.target.lines!!, emptyList(),
- getOffsetFromStringNumber(det.source.position + det.source.size() - 1)
+ getOffsetFromStringNumber(det.source.position + det.source.size() - 1),
+ det.target.position, isInline
)
}
@@ -114,7 +134,7 @@ class Inlayer(val editor: Editor, private val intent: String) : Disposable {
TextAttributes().apply {
backgroundColor = redColor
},
- HighlighterTargetArea.EXACT_RANGE
+ HighlighterTargetArea.LINES_IN_RANGE
)
)
val smallPatches: MutableList> = emptyList>().toMutableList()
@@ -146,7 +166,8 @@ class Inlayer(val editor: Editor, private val intent: String) : Disposable {
renderInsertBlock(
det.target.lines!!,
smallPatches,
- getOffsetFromStringNumber(det.source.position + det.source.size() - 1)
+ getOffsetFromStringNumber(det.source.position + det.source.size() - 1),
+ det.target.position
)
}
@@ -159,7 +180,7 @@ class Inlayer(val editor: Editor, private val intent: String) : Disposable {
TextAttributes().apply {
backgroundColor = redColor
},
- HighlighterTargetArea.EXACT_RANGE
+ HighlighterTargetArea.LINES_IN_RANGE
)
)
}
diff --git a/src/main/kotlin/com/smallcloud/refactai/notifications/Notifications.kt b/src/main/kotlin/com/smallcloud/refactai/notifications/Notifications.kt
index 0e5c593a..b4081ac5 100644
--- a/src/main/kotlin/com/smallcloud/refactai/notifications/Notifications.kt
+++ b/src/main/kotlin/com/smallcloud/refactai/notifications/Notifications.kt
@@ -185,7 +185,7 @@ fun emitRegular(project: Project, editor: Editor) {
val chat = ToolWindowManager.getInstance(project).getToolWindow("Refact")
- if (chat != null && getLSPProcessHolder(project).getCaps().codeChatModels.isNotEmpty()) {
+ if (chat != null && getLSPProcessHolder(project)!!.getCaps().codeChatModels.isNotEmpty()) {
val chatShortcut = KeymapUtil.getShortcutText("ActivateRefactChatToolWindow")
notification.addAction(NotificationAction.createSimple("Chat ($chatShortcut)") {
chat.activate {
diff --git a/src/main/kotlin/com/smallcloud/refactai/panes/RefactAIToolboxPaneFactory.kt b/src/main/kotlin/com/smallcloud/refactai/panes/RefactAIToolboxPaneFactory.kt
index ad8f14ac..dbb28269 100644
--- a/src/main/kotlin/com/smallcloud/refactai/panes/RefactAIToolboxPaneFactory.kt
+++ b/src/main/kotlin/com/smallcloud/refactai/panes/RefactAIToolboxPaneFactory.kt
@@ -32,8 +32,6 @@ class RefactAIToolboxPaneFactory : ToolWindowFactory {
toolWindow.contentManager.addContent(content)
}
-
-
companion object {
private val panesKey = Key.create("refact.panes")
val chat: ChatPanes?
@@ -41,6 +39,7 @@ class RefactAIToolboxPaneFactory : ToolWindowFactory {
val tw = ToolWindowManager.getInstance(getLastUsedProject()).getToolWindow("Refact")
return tw?.contentManager?.getContent(0)?.getUserData(panesKey)
}
+
fun focusChat() {
val tw = ToolWindowManager.getInstance(getLastUsedProject()).getToolWindow("Refact")
val content = tw?.contentManager?.getContent(0) ?: return
@@ -50,4 +49,4 @@ class RefactAIToolboxPaneFactory : ToolWindowFactory {
chat?.newChat()
}
}
-}
\ No newline at end of file
+}
diff --git a/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/ChatPaneInvokeActionPromoter.kt b/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/ChatPaneInvokeActionPromoter.kt
index f7fa6be3..43f5bda6 100644
--- a/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/ChatPaneInvokeActionPromoter.kt
+++ b/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/ChatPaneInvokeActionPromoter.kt
@@ -10,6 +10,7 @@ class ChatPaneInvokeActionPromoter : ActionPromoter {
private fun getEditor(dataContext: DataContext): Editor? {
return CommonDataKeys.EDITOR.getData(dataContext)
}
+
override fun promote(actions: MutableList, context: DataContext): MutableList {
if (getEditor(context) == null)
return actions.toMutableList()
diff --git a/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/ChatPanes.kt b/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/ChatPanes.kt
index f4572957..2a77fb87 100644
--- a/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/ChatPanes.kt
+++ b/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/ChatPanes.kt
@@ -7,7 +7,7 @@ import java.awt.BorderLayout
import javax.swing.JComponent
import javax.swing.JPanel
-class ChatPanes(val project: Project): Disposable {
+class ChatPanes(val project: Project) : Disposable {
private var component: JComponent? = null
private var pane: SharedChatPane? = null
private val holder = JPanel().also {
@@ -31,6 +31,10 @@ class ChatPanes(val project: Project): Disposable {
return holder
}
+ fun executeCodeLensCommand(command: String, sendImmediately: Boolean, openNewTab: Boolean) {
+ pane?.executeCodeLensCommand(command, sendImmediately, openNewTab)
+ }
+
fun requestFocus() {
component?.requestFocus()
}
diff --git a/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/Editor.kt b/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/Editor.kt
index 7a9cdd5a..bf7dd002 100644
--- a/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/Editor.kt
+++ b/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/Editor.kt
@@ -14,14 +14,12 @@ import com.smallcloud.refactai.settings.AppSettingsState
class Editor (val project: Project) {
-
- private val lsp: LSPProcessHolder = LSPProcessHolder.getInstance(project)
+ private val lsp: LSPProcessHolder = LSPProcessHolder.getInstance(project)!!
private fun getLanguage(fm: FileEditorManager): Language? {
val editor = fm.selectedTextEditor
val language = editor?.document?.let {
PsiDocumentManager.getInstance(project).getPsiFile(it)?.language
}
-
return language
}
@@ -84,7 +82,6 @@ class Editor (val project: Project) {
val line2 = selection?.endOffset?.let { editor.offsetToLogicalPosition(it).line } ?: 0
val code = editor?.document?.getText(range)
-
val canPaste = selection != null && !selection.isEmpty
val fileInfo = Events.ActiveFile.FileInfo(
@@ -97,7 +94,6 @@ class Editor (val project: Project) {
content = code,
)
cb(fileInfo)
-
} else {
val fileInfo = Events.ActiveFile.FileInfo()
cb(fileInfo)
diff --git a/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/Events.kt b/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/Events.kt
index 76accd5a..245e5687 100644
--- a/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/Events.kt
+++ b/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/Events.kt
@@ -24,52 +24,63 @@ class EventNames {
LOG_OUT("log_out"),
FIM_READY("fim/ready"),
FIM_REQUEST("fim/request"),
+
// Adding
START_ANIMATION("ide/animateFile/start"),
STOP_ANIMATION("ide/animateFile/stop"),
DIFF_PREVIEW("ide/diffPreview"),
- WRITE_RESULTS_TO_FILE("ide/writeResultsToFile")
+ WRITE_RESULTS_TO_FILE("ide/writeResultsToFile"),
+ IS_CHAT_STREAMING("ide/isChatStreaming"),
+ CHAT_PAGE_CHANGE("ide/chatPageChange"),
}
enum class ToChat(val value: String) {
- @SerializedName("config/update") UPDATE_CONFIG("config/update"),
- @SerializedName("selected_snippet/set") SET_SELECTED_SNIPPET("selected_snippet/set"),
- @SerializedName("activeFile/setFileInfo") SET_ACTIVE_FILE_INFO("activeFile/setFileInfo"),
- @SerializedName("fim/error") FIM_ERROR("fim/error"),
- @SerializedName("fim/receive") FIM_RECEIVE("fim/receive"),
- @SerializedName("chatThread/new") NEW_CHAT("chatThread/new"),
+ @SerializedName("config/update")
+ UPDATE_CONFIG("config/update"),
+ @SerializedName("selected_snippet/set")
+ SET_SELECTED_SNIPPET("selected_snippet/set"),
+ @SerializedName("activeFile/setFileInfo")
+ SET_ACTIVE_FILE_INFO("activeFile/setFileInfo"),
+ @SerializedName("fim/error")
+ FIM_ERROR("fim/error"),
+ @SerializedName("fim/receive")
+ FIM_RECEIVE("fim/receive"),
+ @SerializedName("chatThread/new")
+ NEW_CHAT("chatThread/new"),
+
+ // codelens
+ @SerializedName("textarea/replace")
+ CODE_LENS_EXEC("textarea/replace"),
// logout, open external url, setup host
}
}
-
class Events {
- open class Payload: Serializable
+ open class Payload : Serializable
- abstract class FromChat(val type: EventNames.FromChat, open val payload: Payload?): Serializable
+ abstract class FromChat(val type: EventNames.FromChat, open val payload: Serializable?) : Serializable
private class FromChatDeserializer : JsonDeserializer {
override fun deserialize(p0: JsonElement?, p1: Type?, p2: JsonDeserializationContext?): FromChat? {
val type = p0?.asJsonObject?.get("type")?.asString
-
-
// events without payload
if (type == EventNames.FromChat.LOG_OUT.value) {
return Setup.LogOut()
}
val payload = p0?.asJsonObject?.get("payload")
- if(type == null) return null
+ if (type == null) return null
- return when(type) {
+ return when (type) {
EventNames.FromChat.NEW_FILE.value -> payload?.asString?.let { Editor.NewFile(it) }
EventNames.FromChat.OPEN_SETTINGS.value -> OpenSettings()
EventNames.FromChat.SETUP_HOST.value -> {
val host = p2?.deserialize(payload, Host::class.java) ?: return null
Setup.SetupHost(host)
}
+
EventNames.FromChat.OPEN_EXTERNAL_URL.value -> {
val url = payload?.asJsonObject?.get("url")?.asString ?: return null
Setup.OpenExternalUrl(url)
@@ -81,25 +92,38 @@ class Events {
// EventNames.FromChat.FIM_READY.value -> p2?.deserialize(payload, Fim.Ready::class.java)
EventNames.FromChat.FIM_REQUEST.value -> Fim.Request()
EventNames.FromChat.OPEN_HOTKEYS.value -> OpenHotKeys()
+ EventNames.FromChat.IS_CHAT_STREAMING.value -> {
+ IsChatStreaming(payload?.asBoolean ?: false)
+ }
+
+ EventNames.FromChat.CHAT_PAGE_CHANGE.value -> {
+ ChatPageChange(payload?.asString ?: "")
+ }
+
EventNames.FromChat.OPEN_FILE.value -> {
val file: OpenFilePayload = p2?.deserialize(payload, OpenFilePayload::class.java) ?: return null
OpenFile(file)
}
- EventNames.FromChat.START_ANIMATION.value -> Animation.Start(payload?.asString?: "")
+ EventNames.FromChat.START_ANIMATION.value -> Animation.Start(payload?.asString ?: "")
+
+ EventNames.FromChat.STOP_ANIMATION.value -> Animation.Stop(payload?.asString ?: "")
+
+ EventNames.FromChat.DIFF_PREVIEW.value -> Patch.Show(
+ p2?.deserialize(
+ payload,
+ Patch.ShowPayload::class.java
+ ) ?: return null
+ )
- EventNames.FromChat.STOP_ANIMATION.value -> Animation.Stop(payload?.asString?: "")
-
- EventNames.FromChat.DIFF_PREVIEW.value -> Patch.Show(p2?.deserialize(payload, Patch.ShowPayload::class.java)?: return null)
EventNames.FromChat.WRITE_RESULTS_TO_FILE.value -> {
val results = mutableListOf()
- val items = p2?.deserialize(payload, results::class.java)?: results
+ val items = p2?.deserialize(payload, results::class.java) ?: results
val applyPayload = Patch.ApplyPayload(items)
return Patch.Apply(applyPayload)
}
-
else -> null
}
}
@@ -107,9 +131,11 @@ class Events {
}
class Animation {
- data class AnimationPayload(val payload: String): Payload();
- data class Start(val fileName: String): FromChat(EventNames.FromChat.START_ANIMATION, AnimationPayload(fileName))
- data class Stop(val fileName: String): FromChat(EventNames.FromChat.STOP_ANIMATION, AnimationPayload(fileName))
+ data class AnimationPayload(val payload: String) : Payload();
+ data class Start(val fileName: String) :
+ FromChat(EventNames.FromChat.START_ANIMATION, AnimationPayload(fileName))
+
+ data class Stop(val fileName: String) : FromChat(EventNames.FromChat.STOP_ANIMATION, AnimationPayload(fileName))
}
class Patch {
@@ -133,18 +159,19 @@ class Events {
val allPins: List,
val results: List,
val state: List,
- ): Payload()
+ ) : Payload()
+
+ class Show(override val payload: ShowPayload) : FromChat(EventNames.FromChat.DIFF_PREVIEW, payload)
- class Show(override val payload: ShowPayload): FromChat(EventNames.FromChat.DIFF_PREVIEW, payload)
+ class ApplyPayload(val items: List) : Payload()
- class ApplyPayload(val items: List): Payload()
// typealias ApplyPayload: Payload()
- class Apply(override val payload: ApplyPayload): FromChat(EventNames.FromChat.WRITE_RESULTS_TO_FILE, payload)
+ class Apply(override val payload: ApplyPayload) : FromChat(EventNames.FromChat.WRITE_RESULTS_TO_FILE, payload)
}
class Fim {
- class Ready: FromChat(EventNames.FromChat.FIM_READY, null)
- class Request: FromChat(EventNames.FromChat.FIM_REQUEST, null)
+ class Ready : FromChat(EventNames.FromChat.FIM_READY, null)
+ class Request : FromChat(EventNames.FromChat.FIM_REQUEST, null)
class Choice(
@SerializedName("code_completion")
@@ -250,30 +277,34 @@ class Events {
val created: Number?,
val elapsed: Number?,
val cached: Boolean?,
- ): Payload()
+ ) : Payload()
- class Receive(payload: FimDebugPayload): ToChat(EventNames.ToChat.FIM_RECEIVE, payload)
+ class Receive(payload: FimDebugPayload) : ToChat(EventNames.ToChat.FIM_RECEIVE, payload)
- class Error(payload: String): ToChat(EventNames.ToChat.FIM_ERROR, payload)
+ class Error(payload: String) : ToChat(EventNames.ToChat.FIM_ERROR, payload)
}
- abstract class ToChat(
+ abstract class ToChat(
@SerializedName("type")
val type: EventNames.ToChat,
@SerializedName("payload")
open val payload: T
- ): Serializable
+ ) : Serializable
- class OpenSettings: FromChat(EventNames.FromChat.OPEN_SETTINGS, null)
- class OpenHotKeys: FromChat(EventNames.FromChat.OPEN_HOTKEYS, null)
+ class IsChatStreaming(val isStreaming: Boolean) : FromChat(EventNames.FromChat.IS_CHAT_STREAMING, isStreaming)
+ class ChatPageChange(val currentPage: String) : FromChat(EventNames.FromChat.CHAT_PAGE_CHANGE, currentPage)
+ class OpenSettings : FromChat(EventNames.FromChat.OPEN_SETTINGS, null)
+
+ class OpenHotKeys : FromChat(EventNames.FromChat.OPEN_HOTKEYS, null)
data class OpenFilePayload(
@SerializedName("file_name")
val fileName: String,
- val line: Int?): Payload()
+ val line: Int?
+ ) : Payload()
- class OpenFile(override val payload: OpenFilePayload): FromChat(EventNames.FromChat.OPEN_FILE, payload)
+ class OpenFile(override val payload: OpenFilePayload) : FromChat(EventNames.FromChat.OPEN_FILE, payload)
class ActiveFile {
data class FileInfo(
@@ -287,59 +318,69 @@ class Events {
val usefulness: Int? = null,
)
- class ActiveFileToChat(payload: FileInfo): ToChat(EventNames.ToChat.SET_ACTIVE_FILE_INFO, payload)
+ class ActiveFileToChat(payload: FileInfo) : ToChat(EventNames.ToChat.SET_ACTIVE_FILE_INFO, payload)
}
class Setup {
data class SetupHostPayload(
val host: Host
- ): Payload()
+ ) : Payload()
- data class SetupHost(val host: Host): FromChat(EventNames.FromChat.SETUP_HOST, SetupHostPayload(host))
+ data class SetupHost(val host: Host) : FromChat(EventNames.FromChat.SETUP_HOST, SetupHostPayload(host))
- data class UrlPayload(val url: String): Payload()
- data class OpenExternalUrl(val url: String): FromChat(EventNames.FromChat.OPEN_EXTERNAL_URL, UrlPayload(url))
+ data class UrlPayload(val url: String) : Payload()
+ data class OpenExternalUrl(val url: String) : FromChat(EventNames.FromChat.OPEN_EXTERNAL_URL, UrlPayload(url))
- class LogOut: FromChat(EventNames.FromChat.LOG_OUT, null)
+ class LogOut : FromChat(EventNames.FromChat.LOG_OUT, null)
}
class Editor {
data class ContentPayload(
val payload: String
- ): Payload()
+ ) : Payload()
data class NewFile(
val content: String,
- ): FromChat(EventNames.FromChat.NEW_FILE, ContentPayload(content))
+ ) : FromChat(EventNames.FromChat.NEW_FILE, ContentPayload(content))
data class PasteDiff(
val content: String
- ): FromChat(EventNames.FromChat.PASTE_DIFF, ContentPayload(content))
+ ) : FromChat(EventNames.FromChat.PASTE_DIFF, ContentPayload(content))
data class Snippet(
val language: String = "",
val code: String = "",
val path: String = "",
val basename: String = "",
- ): Payload()
+ ) : Payload()
data class SetSnippetPayload(
val snippet: Snippet
- ): Payload()
-
- class SetSnippetToChat(payload: Snippet): ToChat(EventNames.ToChat.SET_SELECTED_SNIPPET, payload)
+ ) : Payload()
+ class SetSnippetToChat(payload: Snippet) : ToChat(EventNames.ToChat.SET_SELECTED_SNIPPET, payload)
}
- object NewChat: ToChat(EventNames.ToChat.NEW_CHAT, Unit)
+ object NewChat : ToChat(EventNames.ToChat.NEW_CHAT, Unit)
+ data class CodeLensCommandPayload(
+ val value: String = "",
+ @SerializedName("send_immediately") val sendImmediately: Boolean = false,
+ ) : Payload()
+
+ class CodeLensCommand(payload: CodeLensCommandPayload) : ToChat(EventNames.ToChat.CODE_LENS_EXEC, payload)
class Config {
abstract class BaseFeatures()
- data class Features(val ast: Boolean, val vecdb: Boolean): BaseFeatures()
+ data class Features(val ast: Boolean, val vecdb: Boolean) : BaseFeatures()
- data class ThemeProps(val mode: String, val hasBackground: Boolean = false, val scale: String = "90%", val accentColor: String ="gray")
+ data class ThemeProps(
+ val mode: String,
+ val hasBackground: Boolean = false,
+ val scale: String = "90%",
+ val accentColor: String = "gray"
+ )
data class KeyBindings(val completeManual: String)
@@ -352,18 +393,18 @@ class Events {
val keyBindings: Config.KeyBindings,
val tabbed: Boolean? = false,
val host: String? = "jetbrains"
- ): Payload()
+ ) : Payload()
- class Update(payload: UpdatePayload): ToChat(EventNames.ToChat.UPDATE_CONFIG, payload)
+ class Update(payload: UpdatePayload) : ToChat(EventNames.ToChat.UPDATE_CONFIG, payload)
}
companion object {
val gson = GsonBuilder()
- .registerTypeAdapter(FromChat::class.java, FromChatDeserializer())
- .registerTypeAdapter(Host::class.java, HostDeserializer())
- .create()
+ .registerTypeAdapter(FromChat::class.java, FromChatDeserializer())
+ .registerTypeAdapter(Host::class.java, HostDeserializer())
+ .create()
fun parse(msg: String?): FromChat? {
val result = gson.fromJson(msg, FromChat::class.java)
diff --git a/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/SharedChatPane.kt b/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/SharedChatPane.kt
index f848fedb..c8f90452 100644
--- a/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/SharedChatPane.kt
+++ b/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/SharedChatPane.kt
@@ -5,6 +5,7 @@ import com.intellij.execution.processTools.getResultStdoutStr
import com.intellij.ide.BrowserUtil
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.application.invokeLater
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.editor.LogicalPosition
@@ -16,6 +17,7 @@ import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.vfs.LocalFileSystem
+import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.testFramework.LightVirtualFile
@@ -43,6 +45,7 @@ import kotlinx.coroutines.runBlocking
import org.jetbrains.annotations.NotNull
import java.beans.PropertyChangeListener
import java.io.File
+import java.util.concurrent.Future
import javax.swing.JPanel
import javax.swing.UIManager
@@ -50,10 +53,34 @@ import javax.swing.UIManager
class SharedChatPane(val project: Project) : JPanel(), Disposable {
private val logger = Logger.getInstance(SharedChatPane::class.java)
private val editor = Editor(project)
+ private var currentPage: String = ""
+ private var isChatStreaming: Boolean = false
var id: String? = null;
- val animatedFiles = mutableSetOf()
+ private val animatedFiles = mutableSetOf()
private val scheduler = AppExecutorUtil.createBoundedScheduledExecutorService("SMCRainbowScheduler", 2)
+ private val messageQuery: ArrayDeque> = ArrayDeque>()
+ private var workerFuture: Future<*>? = null
+ private val workerScheduler =
+ AppExecutorUtil.createBoundedScheduledExecutorService("SMCSharedChatPaneWorkerScheduler", 1)
+
+ init {
+ workerFuture = workerScheduler.scheduleWithFixedDelay({
+ synchronized(this) {
+ while (isReady() && messageQuery.isNotEmpty()) {
+ messageQuery.removeFirst().let {
+ this.browser.postMessage(it)
+ }
+ }
+ }
+ }, 0, 80, java.util.concurrent.TimeUnit.MILLISECONDS)
+ this.addEventListeners()
+ }
+
+ private fun isReady(): Boolean {
+ return currentPage.isNotEmpty() // didn't get first message
+ }
+
fun newChat() {
this.postMessage(Events.NewChat)
}
@@ -67,6 +94,20 @@ class SharedChatPane(val project: Project) : JPanel(), Disposable {
}
}
+ fun executeCodeLensCommand(command: String, sendImmediately: Boolean, openNewTab: Boolean) {
+ if (isChatStreaming) return
+ if (openNewTab || this.currentPage != "chat") {
+ newChat()
+ }
+ if (command.isEmpty()) {
+ // Just opening a new chat, no codelens execution
+ newChat()
+ return
+ }
+ isChatStreaming = true
+ this.postMessage(Events.CodeLensCommand(Events.CodeLensCommandPayload(command, sendImmediately)))
+ }
+
private fun sendUserConfig() {
val config = this.editor.getUserConfig()
val message = Events.Config.Update(config)
@@ -112,7 +153,8 @@ class SharedChatPane(val project: Project) : JPanel(), Disposable {
val fileName = out.lines().last()
ApplicationManager.getApplication().invokeLater {
- val virtualFile: VirtualFile? = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(File(fileName))
+ val virtualFile: VirtualFile? =
+ LocalFileSystem.getInstance().refreshAndFindFileByIoFile(File(fileName))
if (virtualFile != null) {
// Open the file in the editor
FileEditorManager.getInstance(project).openFile(virtualFile, true)
@@ -175,7 +217,6 @@ class SharedChatPane(val project: Project) : JPanel(), Disposable {
this@SharedChatPane.sendActiveFileInfo()
this@SharedChatPane.sendSelectedSnippet()
}
-
}
project.messageBus.connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, listener)
@@ -243,7 +284,6 @@ class SharedChatPane(val project: Project) : JPanel(), Disposable {
}
}
-
private fun handleOpenSettings() {
ApplicationManager.getApplication().invokeLater {
ShowSettingsUtil.getInstance().showSettingsDialog(project, AppSettingsConfigurable::class.java)
@@ -275,22 +315,20 @@ class SharedChatPane(val project: Project) : JPanel(), Disposable {
private fun handleOpenFile(fileName: String, line: Int?) {
val sanitizedFileName = this.sanitizeFileNameForPosix(fileName)
val file = File(sanitizedFileName)
- val vf = ApplicationManager.getApplication().runReadAction {
- VfsUtil.findFileByIoFile(file, true)
- } ?: return
-
- val fileDescriptor = OpenFileDescriptor(project, vf)
-
- ApplicationManager.getApplication().invokeLater {
+ logger.warn("handleOpenFile: $fileName")
+ invokeLater {
+ val vf = VfsUtil.findFileByIoFile(file, true) ?: return@invokeLater
+ val fileDescriptor = OpenFileDescriptor(project, vf)
val editor = FileEditorManager.getInstance(project).openTextEditor(fileDescriptor, true)
+ logger.warn("handleOpenFile: $fileName found")
line?.let {
editor?.caretModel?.moveToLogicalPosition(LogicalPosition(line, 0))
}
}
-
}
private fun deleteFile(fileName: String) {
+ logger.warn("deleteFile: $fileName")
ApplicationManager.getApplication().runReadAction {
LocalFileSystem.getInstance().findFileByPath(fileName)?.delete(this.project)
}
@@ -312,98 +350,134 @@ class SharedChatPane(val project: Project) : JPanel(), Disposable {
return fileName
}
- private fun openNewFileWithContent(fileName: String, content: String) {
- val virtualFile = LightVirtualFile(fileName, content)
- val fileDescriptor = OpenFileDescriptor(project, virtualFile)
+ private fun openNewFile(fileName: String) {
+ val sanitizedFileName = this.sanitizeFileNameForPosix(fileName)
+ val file = File(sanitizedFileName)
+ if (!file.exists()) {
+ file.createNewFile()
+ }
+ val fileSystem = StandardFileSystems.local()
+ fileSystem.refresh(false)
+ logger.warn("openNewFileWithContent: $fileName")
+ }
+
+ private fun setContent(fileName: String, content: String) {
+ logger.warn("setContent: item.fileNameEdit = $fileName")
+ val file = ApplicationManager.getApplication().runReadAction {
+ LocalFileSystem.getInstance().refreshAndFindFileByPath(fileName)
+ }
+ if (file == null) {
+ logger.warn("setContent: item.fileNameEdit = $fileName is null")
+ return
+ }
+
ApplicationManager.getApplication().invokeLater {
- FileEditorManager.getInstance(project).openTextEditor(fileDescriptor, true)
+ FileDocumentManager.getInstance().getDocument(file)?.setText(content)
}
}
private fun handlePatchApply(payload: Events.Patch.ApplyPayload) {
payload.items.forEach { item ->
if (item.fileNameAdd != null) {
- this.openNewFileWithContent(this.sanitizeFileNameForPosix(item.fileNameAdd), item.fileText)
+ val fileName = this.sanitizeFileNameForPosix(item.fileNameAdd)
+ logger.warn("handlePatchApply: item.fileNameAdd = $fileName")
+ this.openNewFile(fileName)
+ setContent(fileName, item.fileText)
}
if (item.fileNameDelete != null) {
- this.deleteFile(this.sanitizeFileNameForPosix(item.fileNameDelete))
+ val fileName = this.sanitizeFileNameForPosix(item.fileNameDelete)
+ logger.warn("handlePatchApply: item.fileNameDelete = $fileName")
+ this.deleteFile(fileName)
}
if (item.fileNameEdit != null) {
- val file = ApplicationManager.getApplication().runReadAction {
- LocalFileSystem.getInstance().findFileByPath(this.sanitizeFileNameForPosix(item.fileNameEdit))
- } ?: return
+ val fileName = this.sanitizeFileNameForPosix(item.fileNameEdit)
+ logger.warn("handlePatchApply: item.fileNameEdit = $fileName")
+ setContent(fileName, item.fileText)
+ }
+ }
+ }
- ApplicationManager.getApplication().invokeLater {
- FileDocumentManager.getInstance().getDocument(file)?.setText(item.fileText)
- }
+ private fun showPatch(fileName: String, fileText: String) {
+ logger.warn("showPatch: item.fileNameEdit = $fileName")
+ this.handleAnimationStop(fileName)
+ val file = ApplicationManager.getApplication().runReadAction {
+ LocalFileSystem.getInstance().refreshAndFindFileByPath(fileName)
+ }
+ if (file == null) {
+ logger.warn("showPatch: item.fileNameEdit = $fileName is null")
+ return
+ }
+
+ val fileDescriptor = OpenFileDescriptor(project, file)
+ ApplicationManager.getApplication().invokeLater {
+ val editor = FileEditorManager.getInstance(project).openTextEditor(fileDescriptor, true)
+ editor?.selectionModel?.setSelection(0, editor.document.textLength)
+ if (editor != null) {
+ ModeProvider.getOrCreateModeProvider(editor).getDiffMode()
+ .actionPerformed(editor, fileText)
}
}
}
private fun handlePatchShow(payload: Events.Patch.ShowPayload) {
-
-
payload.results.forEach { result ->
if (result.fileNameAdd != null) {
- this.openNewFileWithContent(this.sanitizeFileNameForPosix(result.fileNameAdd), result.fileText)
+ val sanitizedFileNameEdit = this.sanitizeFileNameForPosix(result.fileNameAdd)
+ logger.warn("handlePatchShow: item.fileNameAdd = $sanitizedFileNameEdit")
+ this.openNewFile(sanitizedFileNameEdit)
+ showPatch(sanitizedFileNameEdit, result.fileText)
}
- if (result.fileNameDelete!= null) {
+ if (result.fileNameDelete != null) {
+ logger.warn("handlePatchShow: item.fileNameDelete = ${this.sanitizeFileNameForPosix(result.fileNameDelete)}")
this.deleteFile(this.sanitizeFileNameForPosix(result.fileNameDelete))
}
if (result.fileNameEdit != null) {
val sanitizedFileNameEdit = this.sanitizeFileNameForPosix(result.fileNameEdit)
- this.handleAnimationStop(sanitizedFileNameEdit)
-
- val file = ApplicationManager.getApplication().runReadAction {
- LocalFileSystem.getInstance().findFileByPath(sanitizedFileNameEdit)
- } ?: return
-
- val fileDescriptor = OpenFileDescriptor(project, file)
- ApplicationManager.getApplication().invokeLater {
- val editor = FileEditorManager.getInstance(project).openTextEditor(fileDescriptor, true)
- editor?.selectionModel?.setSelection(0, editor.document.textLength)
- if (editor != null) {
- ModeProvider.getOrCreateModeProvider(editor).getDiffMode()
- .actionPerformed(editor, result.fileText)
- }
- }
-
+ logger.warn("handlePatchShow: item.fileNameEdit = $sanitizedFileNameEdit")
+ showPatch(sanitizedFileNameEdit, result.fileText)
}
-
}
}
-
private fun handleAnimationStart(fileName: String) {
- val sanitizedFileName = this.sanitizeFileNameForPosix(fileName)
- val file = ApplicationManager.getApplication().runReadAction {
- LocalFileSystem.getInstance().findFileByPath(sanitizedFileName)
- } ?: return
- val fileDescriptor = OpenFileDescriptor(project, file)
- ApplicationManager.getApplication().invokeLater {
- val editor = FileEditorManager.getInstance(project).openTextEditor(fileDescriptor, true) ?: return@invokeLater
- // editor.selectionModel.setSelection(0, editor.document.textLength)
+ synchronized(this) { // action thread
+ val sanitizedFileName = this.sanitizeFileNameForPosix(fileName)
+ if (animatedFiles.contains(sanitizedFileName)) return
animatedFiles.add(sanitizedFileName)
- scheduler.submit {
- waitingDiff(
- editor,
- editor.offsetToLogicalPosition(0),
- editor.offsetToLogicalPosition(editor.document.textLength),
- { -> animatedFiles.contains(sanitizedFileName) }
- )
+ val file = ApplicationManager.getApplication().runReadAction {
+ LocalFileSystem.getInstance().findFileByPath(sanitizedFileName)
+ } ?: return
+ val fileDescriptor = OpenFileDescriptor(project, file)
+ ApplicationManager.getApplication().invokeLater {
+ val editor =
+ FileEditorManager.getInstance(project).openTextEditor(fileDescriptor, true) ?: return@invokeLater
+ scheduler.submit {
+ waitingDiff(
+ editor,
+ editor.offsetToLogicalPosition(0),
+ editor.offsetToLogicalPosition(editor.document.textLength)
+ ) {
+ synchronized(this) {
+ animatedFiles.contains(sanitizedFileName)
+ }
+ }
+ }
}
}
}
private fun handleAnimationStop(fileName: String) {
- animatedFiles.remove(fileName)
+ synchronized(this) {
+ animatedFiles.remove(fileName)
+ }
}
private suspend fun handleEvent(event: Events.FromChat) {
+ logger.warn("${event.toString()} ${event.payload.toString()}")
when (event) {
is Events.Editor.PasteDiff -> this.handlePasteDiff(event.content)
is Events.Editor.NewFile -> this.handleNewFile(event.content)
@@ -418,6 +492,13 @@ class SharedChatPane(val project: Project) : JPanel(), Disposable {
is Events.Patch.Show -> this.handlePatchShow(event.payload)
is Events.Animation.Start -> this.handleAnimationStart(event.fileName)
is Events.Animation.Stop -> this.handleAnimationStop(event.fileName)
+ is Events.IsChatStreaming -> {
+ isChatStreaming = event.payload as Boolean
+ }
+
+ is Events.ChatPageChange -> {
+ currentPage = event.payload.toString()
+ }
else -> Unit
}
@@ -433,24 +514,25 @@ class SharedChatPane(val project: Project) : JPanel(), Disposable {
}
}
-
val webView by lazy {
browser.webView
}
private fun postMessage(message: Events.ToChat<*>?) {
- this.browser.postMessage(message)
+ synchronized(this) {
+ if (message != null) {
+ messageQuery.add(message)
+ }
+ }
}
-
override fun dispose() {
UIManager.removePropertyChangeListener(uiChangeListener)
webView.dispose()
+ scheduler.shutdownNow()
+ workerFuture?.cancel(true)
+ workerScheduler.shutdownNow()
Disposer.dispose(this)
}
- init {
- this.addEventListeners()
- }
}
-
diff --git a/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/browser/ChatWebView.kt b/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/browser/ChatWebView.kt
index 59c3e7cd..f9e214ba 100644
--- a/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/browser/ChatWebView.kt
+++ b/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/browser/ChatWebView.kt
@@ -8,23 +8,11 @@ import com.intellij.openapi.keymap.KeymapManager
import com.intellij.openapi.keymap.KeymapUtil
import com.intellij.ui.jcef.*
import com.intellij.util.ui.UIUtil
-import com.smallcloud.refactai.PluginState
-import com.smallcloud.refactai.lsp.LSPProcessHolderChangedNotifier
import com.smallcloud.refactai.panes.sharedchat.Editor
import com.smallcloud.refactai.panes.sharedchat.Events
import org.cef.CefApp
import org.cef.browser.CefBrowser
-import org.cef.browser.CefFrame
-import org.cef.callback.CefAuthCallback
-import org.cef.callback.CefCallback
-import org.cef.handler.*
-import org.cef.misc.BoolRef
-import org.cef.misc.StringRef
-import org.cef.network.CefCookie
-import org.cef.network.CefRequest
-import org.cef.network.CefResponse
-import org.cef.network.CefURLRequest
-import org.cef.security.CefSSLInfo
+import org.cef.handler.CefLoadHandlerAdapter
import javax.swing.JComponent
@@ -71,8 +59,6 @@ class ChatWebView(val editor: Editor, val messageHandler: (event: Events.FromCha
}
val webView by lazy {
-
-
val browser = JBCefBrowser
.createBuilder()
.setEnableOpenDevToolsMenuItem(true)
@@ -83,13 +69,6 @@ class ChatWebView(val editor: Editor, val messageHandler: (event: Events.FromCha
.setOffScreenRendering(true)
.build()
- browser.jbCefClient.addDisplayHandler(object : CefDisplayHandlerAdapter() {
- override fun onCursorChange(browser: CefBrowser?, cursorType: Int): Boolean {
- browser?.uiComponent?.cursor = java.awt.Cursor.getPredefinedCursor(cursorType);
- return false
- }
- }, browser.cefBrowser)
-
browser.jbCefClient.setProperty(
JBCefClient.Properties.JS_QUERY_POOL_SIZE,
jsPoolSize,
@@ -98,7 +77,6 @@ class ChatWebView(val editor: Editor, val messageHandler: (event: Events.FromCha
browser.setProperty(JBCefBrowserBase.Properties.NO_CONTEXT_MENU, true)
}
-
CefApp.getInstance().registerSchemeHandlerFactory("http", "refactai", RequestHandlerFactory())
val myJSQueryOpenInBrowser = JBCefJSQuery.create((browser as JBCefBrowserBase?)!!)
diff --git a/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/browser/RequestHandler.kt b/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/browser/RequestHandler.kt
index 5d2abbbc..f06764a4 100644
--- a/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/browser/RequestHandler.kt
+++ b/src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/browser/RequestHandler.kt
@@ -25,6 +25,7 @@ class RequestHandlerFactory : CefSchemeHandlerFactory {
return RefactChatResourceHandler()
}
}
+
data object ClosedConnection : ResourceHandlerState() {
override fun getResponseHeaders(
cefResponse: CefResponse,
@@ -141,7 +142,6 @@ class RefactChatResourceHandler : CefResourceHandler, DumbAware {
responseLength: IntRef,
redirectUrl: StringRef
) {
-
if (currentUrl !== null) {
when {
currentUrl!!.contains("css") -> cefResponse.mimeType = "text/css"
diff --git a/src/main/kotlin/com/smallcloud/refactai/settings/AppSettingsState.kt b/src/main/kotlin/com/smallcloud/refactai/settings/AppSettingsState.kt
index e48bca51..48746e77 100644
--- a/src/main/kotlin/com/smallcloud/refactai/settings/AppSettingsState.kt
+++ b/src/main/kotlin/com/smallcloud/refactai/settings/AppSettingsState.kt
@@ -46,7 +46,7 @@ class AppSettingsState : PersistentStateComponent {
var rateUsNotification: Boolean = false
var astIsEnabled: Boolean = true
var astIsEnabledDefaultChanged: Boolean = false
- var vecdbIsEnabled: Boolean = false
+ var vecdbIsEnabled: Boolean = true
var vecdbIsEnabledDefaultChanged: Boolean = false
var astFileLimit: Int = 15000
var vecdbFileLimit: Int = 15000
diff --git a/src/main/kotlin/com/smallcloud/refactai/statistic/UsageStats.kt b/src/main/kotlin/com/smallcloud/refactai/statistic/UsageStats.kt
index afbc5b81..6873c5fe 100644
--- a/src/main/kotlin/com/smallcloud/refactai/statistic/UsageStats.kt
+++ b/src/main/kotlin/com/smallcloud/refactai/statistic/UsageStats.kt
@@ -45,7 +45,7 @@ class UsageStats(private val project: Project): Disposable {
"url" to relatedUrl,
)
)
- val url = getLSPProcessHolder(project).url.resolve(defaultReportUrlSuffix)
+ val url = getLSPProcessHolder(project)!!.url.resolve(defaultReportUrlSuffix)
execService.submit {
try {
val res = sendRequest(url, "POST", body=body)
@@ -63,7 +63,7 @@ class UsageStats(private val project: Project): Disposable {
}
fun snippetAccepted(snippetId: Int) {
- val url = getLSPProcessHolder(project).url.resolve(defaultSnippetAcceptedUrlSuffix)
+ val url = getLSPProcessHolder(project)!!.url.resolve(defaultSnippetAcceptedUrlSuffix)
execService.submit {
try {
val gson = Gson()
diff --git a/src/main/kotlin/com/smallcloud/refactai/status_bar/StatusBarWidget.kt b/src/main/kotlin/com/smallcloud/refactai/status_bar/StatusBarWidget.kt
index c94b0e90..a90901f6 100644
--- a/src/main/kotlin/com/smallcloud/refactai/status_bar/StatusBarWidget.kt
+++ b/src/main/kotlin/com/smallcloud/refactai/status_bar/StatusBarWidget.kt
@@ -57,7 +57,7 @@ class StatusBarState {
class SMCStatusBarWidget(project: Project) : EditorBasedWidget(project), CustomStatusBarWidget, WidgetPresentation {
private var component: StatusBarComponent? = null
private var logger: Logger = Logger.getInstance(javaClass)
- private val lsp: LSPProcessHolder = LSPProcessHolder.getInstance(project)
+ private val lsp: LSPProcessHolder = LSPProcessHolder.getInstance(project)!!
private val spinIcon = AnimatedIcon.Default.INSTANCE
private fun getVirtualFile(editor: Editor): VirtualFile? {
diff --git a/src/main/kotlin/com/smallcloud/refactai/struct/ChatMessage.kt b/src/main/kotlin/com/smallcloud/refactai/struct/ChatMessage.kt
new file mode 100644
index 00000000..48e7ad18
--- /dev/null
+++ b/src/main/kotlin/com/smallcloud/refactai/struct/ChatMessage.kt
@@ -0,0 +1,8 @@
+package com.smallcloud.refactai.struct
+
+import com.google.gson.annotations.SerializedName
+
+data class ChatMessage(val role: String,
+ val content: String,
+ @SerializedName("tool_call_id") val toolCallId: String,
+ val usage: String?)
\ No newline at end of file
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index 87d409e8..65c8195c 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -106,6 +106,9 @@ integrated into a single package that follows your privacy settings.
+
+
+
+
+
The chat feature could not start in your JetBrains IDE. Please refer to the documentation for setup and troubleshooting steps.