Skip to content

Commit

Permalink
add codelens
Browse files Browse the repository at this point in the history
wip

Wip2
  • Loading branch information
reymondzzzz committed Oct 27, 2024
1 parent 11cf255 commit 41b7d24
Show file tree
Hide file tree
Showing 12 changed files with 298 additions and 31 deletions.
2 changes: 1 addition & 1 deletion src/main/kotlin/com/smallcloud/refactai/Resources.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.smallcloud.refactai.code_lens

import com.intellij.openapi.actionSystem.AnActionEvent
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 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)
}

fun actionPerformed() {
val chat = editor.project?.let { ToolWindowManager.getInstance(it).getToolWindow("Refact") }
chat?.activate {
RefactAIToolboxPaneFactory.chat?.requestFocus()
RefactAIToolboxPaneFactory.chat?.executeCodeLensCommand(formatMessage(), sendImmediately, openNewTab)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
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

data class CodeLen(
val range: TextRange,
val label: String,
val action: CodeLensAction
)

class RefactCodeVisionProvider(private val commandKey: String, private val posAfter: String?, private val label: String) :
CodeVisionProvider<Unit> {
override val defaultAnchor: CodeVisionAnchorKind
get() = CodeVisionAnchorKind.Top
override val id: String
get() = "refactai.codelens.$commandKey"
override val name: String
get() = "Refact.ai Hint($label)"
override val relativeOrderings: List<CodeVisionRelativeOrdering>
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<CodeLen> {
val codeLensStr = lspGetCodeLens(editor)
val gson = Gson()
val customization = getInstance(editor.project!!).fetchCustomization()
val codeLensJson = gson.fromJson(codeLensStr, JsonObject::class.java)
val resCodeLenses = mutableListOf<CodeLen>()
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 codeLens = getCodeLens(editor)
return runReadAction {
val result = ArrayList<Pair<TextRange, CodeVisionEntry>>()
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@runReadAction CodeVisionState.Ready(result)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.smallcloud.refactai.code_lens

import com.intellij.codeInsight.codeVision.CodeVisionProvider
import com.intellij.codeInsight.codeVision.CodeVisionProviderFactory
import com.intellij.openapi.project.Project
import com.smallcloud.refactai.lsp.LSPProcessHolder.Companion.getInstance

class RefactCodeVisionProviderFactory : CodeVisionProviderFactory {
override fun createProviders(project: Project): Sequence<CodeVisionProvider<*>> {
val customization = getInstance(project).fetchCustomization()
if (customization.has("code_lens")) {
val allCodeLenses = customization.get("code_lens").asJsonObject
val allCodeLensKeys = allCodeLenses.keySet().toList()
val providers: MutableList<CodeVisionProvider<*>> = 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))
}
return providers.asSequence()

}
return emptySequence()
}
}
28 changes: 26 additions & 2 deletions src/main/kotlin/com/smallcloud/refactai/lsp/LSPHelper.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -75,4 +75,28 @@ fun lspSetActiveDocument(editor: Editor) {
InferenceGlobalContext.lastErrorMsg = it.message
}
})
}
}


fun lspGetCodeLens(editor: Editor): String {
val project = editor.project!!
val url = getLSPProcessHolder(project).url.resolve("/v1/code-lens")
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
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -264,7 +265,7 @@ class LSPProcessHolder(val project: Project) : Disposable {
InferenceGlobalContext.connection.ping(url)
buildInfo = getBuildInfo()
capabilities = getCaps()
fetchToolboxConfig()
fetchCustomization()
isWorking = true
break
} catch (e: Exception) {
Expand All @@ -276,7 +277,7 @@ class LSPProcessHolder(val project: Project) : Disposable {
lspProjectInitialize(this, project)
}

private fun fetchToolboxConfig(): String {
fun fetchCustomization(): JsonObject {
val config = InferenceGlobalContext.connection.get(url.resolve("/v1/customization"),
dataReceiveEnded={
InferenceGlobalContext.status = ConnectionStatus.CONNECTED
Expand All @@ -289,7 +290,7 @@ class LSPProcessHolder(val project: Project) : Disposable {
InferenceGlobalContext.lastErrorMsg = it.message
}
}).join().get()
return config as String
return Gson().fromJson(config as String, JsonObject::class.java)
}

private fun safeTerminate() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,14 @@ class RefactAIToolboxPaneFactory : ToolWindowFactory {
toolWindow.contentManager.addContent(content)
}



companion object {
private val panesKey = Key.create<ChatPanes>("refact.panes")
val chat: ChatPanes?
get() {
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
Expand All @@ -50,4 +49,4 @@ class RefactAIToolboxPaneFactory : ToolWindowFactory {
chat?.newChat()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
22 changes: 17 additions & 5 deletions src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/Events.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ class EventNames {
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) {
Expand All @@ -38,6 +40,8 @@ class EventNames {
@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
}
}
Expand All @@ -48,13 +52,11 @@ class Events {

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<FromChat> {
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()
Expand All @@ -81,6 +83,8 @@ 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)
Expand Down Expand Up @@ -264,6 +268,10 @@ class Events {
open val payload: T
): Serializable



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)
Expand Down Expand Up @@ -329,10 +337,14 @@ class Events {
): Payload()

class SetSnippetToChat(payload: Snippet): ToChat<Payload>(EventNames.ToChat.SET_SELECTED_SNIPPET, payload)

}

object NewChat: ToChat<Unit>(EventNames.ToChat.NEW_CHAT, Unit)
data class CodeLensCommandPayload(
val value: String = "",
@SerializedName("send_immediately") val sendImmediately: Boolean = false,
): Payload()
class CodeLensCommand(payload: CodeLensCommandPayload): ToChat<Payload>(EventNames.ToChat.CODE_LENS_EXEC, payload)

class Config {
abstract class BaseFeatures()
Expand Down
Loading

0 comments on commit 41b7d24

Please sign in to comment.