Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add codelens #186

Merged
merged 26 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .idea/kotlinc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ 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
#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
Expand Down
2 changes: 1 addition & 1 deletion refact_lsp
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v0.10.2
jb
2 changes: 2 additions & 0 deletions src/main/kotlin/com/smallcloud/refactai/Initializer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down
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,63 @@
package com.smallcloud.refactai.code_lens

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.invokeLater
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)
}
// If content is empty, then it's "Open Chat" instruction, selecting range of code in active tab
if (contentMsg.isEmpty()) {
invokeLater {
val pos1 = LogicalPosition(line1, 0)
val pos2 = LogicalPosition(line2, editor.document.getLineEndOffset(line2))

editor.selectionModel.setSelection(
editor.logicalPositionToOffset(pos1),
editor.logicalPositionToOffset(pos2)
)
}
}
}
}
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.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.project.Project
import com.smallcloud.refactai.lsp.LSPProcessHolderChangedNotifier

class CodeLensInvalidatorService(project: Project): Disposable {
private var ids: List<String> = emptyList()
override fun dispose() {}
fun setCodeLensIds(ids: List<String>) {
this.ids = ids
}

init {
project.messageBus.connect(this).subscribe(LSPProcessHolderChangedNotifier.TOPIC, object : LSPProcessHolderChangedNotifier {
override fun lspIsActive(isActive: Boolean) {
invokeLater {
project.service<CodeVisionInitializer>().getCodeVisionHost()
.invalidateProvider(CodeVisionHost.LensInvalidateSignal(null, ids))
}
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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
)

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<Unit> {
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<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 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 lsp = editor.project?.let { getInstance(it) } ?: return CodeVisionState.NotReady
if (!lsp.isWorking) return CodeVisionState.NotReady
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,40 @@
package com.smallcloud.refactai.code_lens

import com.google.gson.JsonObject
import com.intellij.codeInsight.codeVision.CodeVisionProvider
import com.intellij.codeInsight.codeVision.CodeVisionProviderFactory
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.smallcloud.refactai.lsp.LSPProcessHolder.Companion.getCustomizationDirectly
import com.smallcloud.refactai.lsp.LSPProcessHolder.Companion.initialize

class RefactCodeVisionProviderFactory : CodeVisionProviderFactory {
val customization: JsonObject?
init {
initialize()
customization = getCustomizationDirectly()
}

override fun createProviders(project: Project): Sequence<CodeVisionProvider<*>> {
if (customization == null) return emptySequence()
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, customization))
}
val ids = providers.map { it.id }
project.service<CodeLensInvalidatorService>().setCodeLensIds(ids)

return providers.asSequence()

}
return emptySequence()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,71 +5,55 @@ 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 {
val TOPIC = Topic.create("Selection Changed Notifier", SelectionChangedNotifier::class.java)
}
}

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>(FileEditorManagerListener.FILE_EDITOR_MANAGER, this)
.messageBus.connect(PluginState.instance)
.subscribe<FileEditorManagerListener>(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) {
Expand Down
Loading