├── .idea ├── .name ├── .gitignore ├── vcs.xml ├── kotlinc.xml └── gradle.xml ├── refact_lsp ├── settings.gradle.kts ├── almost-all-features-05x.jpg ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── src ├── main │ ├── kotlin │ │ └── com │ │ │ └── smallcloud │ │ │ └── refactai │ │ │ ├── struct │ │ │ ├── Exceptions.kt │ │ │ ├── DeploymentMode.kt │ │ │ ├── ChatMessage.kt │ │ │ ├── SMCPrediction.kt │ │ │ └── SMCRequest.kt │ │ │ ├── statistic │ │ │ └── UsageStatistic.kt │ │ │ ├── utils │ │ │ ├── getExtension.kt │ │ │ ├── LastProjectGetter.kt │ │ │ ├── LinksPanel.kt │ │ │ ├── OSRRenderer.kt │ │ │ ├── JSQueryManager.kt │ │ │ ├── CefLifecycleManager.kt │ │ │ ├── AsyncMessageHandler.kt │ │ │ └── JavaScriptExecutor.kt │ │ │ ├── modes │ │ │ ├── diff │ │ │ │ ├── Utils.kt │ │ │ │ ├── renderer │ │ │ │ │ ├── RenderHelper.kt │ │ │ │ │ ├── BlockRenderer.kt │ │ │ │ │ └── PanelRenderer.kt │ │ │ │ ├── waitingDiff.kt │ │ │ │ ├── DiffLayout.kt │ │ │ │ └── DiffMode.kt │ │ │ ├── completion │ │ │ │ ├── structs │ │ │ │ │ ├── Completion.kt │ │ │ │ │ └── DocumentEventExtra.kt │ │ │ │ ├── CompletionTracker.kt │ │ │ │ ├── StubCompletionMode.kt │ │ │ │ └── prompt │ │ │ │ │ └── RequestCreator.kt │ │ │ ├── Mode.kt │ │ │ ├── EventAdapter.kt │ │ │ └── EditorTextState.kt │ │ │ ├── listeners │ │ │ ├── PluginListener.kt │ │ │ ├── InlineActionPromoter.kt │ │ │ ├── GlobalCaretListener.kt │ │ │ ├── GlobalFocusListener.kt │ │ │ ├── CancelActionPromoter.kt │ │ │ ├── ForceCompletionActionPromoter.kt │ │ │ ├── AcceptActionPromoter.kt │ │ │ ├── UninstallListener.kt │ │ │ ├── LSPDocumentListener.kt │ │ │ ├── CancelAction.kt │ │ │ ├── ForceCompletionAction.kt │ │ │ ├── DocumentListener.kt │ │ │ ├── LastEditorGetterListener.kt │ │ │ ├── AcceptAction.kt │ │ │ └── GenerateGitCommitMessageAction.kt │ │ │ ├── RefactAIBundle.kt │ │ │ ├── io │ │ │ ├── Connection.kt │ │ │ ├── Fetch.kt │ │ │ ├── InferenceGlobalContextChangedNotifier.kt │ │ │ ├── CloudMessageService.kt │ │ │ └── RequestHelpers.kt │ │ │ ├── account │ │ │ ├── AccountManagerChangedNotifier.kt │ │ │ └── AccountManager.kt │ │ │ ├── notifications │ │ │ └── Initializer.kt │ │ │ ├── panes │ │ │ ├── sharedchat │ │ │ │ ├── ChatPaneInvokeActionPromoter.kt │ │ │ │ ├── ChatPaneInvokeAction.kt │ │ │ │ ├── ChatPanes.kt │ │ │ │ └── Editor.kt │ │ │ └── RefactAIToolboxPaneFactory.kt │ │ │ ├── lsp │ │ │ ├── LSPActiveDocNotifierService.kt │ │ │ ├── LSPTools.kt │ │ │ ├── RagStatus.kt │ │ │ ├── LSPCapabilities.kt │ │ │ └── LSPConfig.kt │ │ │ ├── FimCache.kt │ │ │ ├── status_bar │ │ │ ├── StatusBarProvider.kt │ │ │ └── StatusBarComponent.kt │ │ │ ├── code_lens │ │ │ ├── CodeLensInvalidatorService.kt │ │ │ ├── RefactCodeVisionProviderFactory.kt │ │ │ ├── CodeLensAction.kt │ │ │ └── RefactCodeVisionProvider.kt │ │ │ ├── codecompletion │ │ │ ├── RefactInlineCompletionDocumentListener.kt │ │ │ └── RefactAIContinuousEvent.kt │ │ │ ├── PluginState.kt │ │ │ ├── settings │ │ │ └── Host.kt │ │ │ ├── Initializer.kt │ │ │ ├── PluginErrorReportSubmitter.kt │ │ │ ├── Resources.kt │ │ │ └── UpdateChecker.kt │ └── resources │ │ ├── webview │ │ └── index.html │ │ ├── icons │ │ ├── hand_12x12.svg │ │ ├── refactai_logo_red_12x12.svg │ │ ├── refactai_logo_12x12.svg │ │ ├── refactai_logo_red_13x13.svg │ │ ├── refactai_logo_red_16x16.svg │ │ └── coin_16x16.svg │ │ ├── META-INF │ │ └── pluginIcon.svg │ │ └── bundles │ │ └── RefactAI.properties └── test │ └── kotlin │ └── com │ └── smallcloud │ └── refactai │ ├── panes │ └── sharedchat │ │ ├── ChatWebViewTestSuite.kt │ │ └── BasicChatWebViewTest.kt │ ├── lsp │ ├── LspToolsTest.kt │ ├── LSPProcessHolderTimeoutTest.kt │ └── LSPProcessHolderTest.kt │ ├── io │ └── AsyncConnectionTest.kt │ └── testUtils │ ├── MockServer.kt │ └── TestableChatWebView.kt ├── .gitignore ├── LICENSE ├── gradle.properties ├── CONTRIBUTING.md ├── README.md └── gradlew.bat /.idea/.name: -------------------------------------------------------------------------------- 1 | refact -------------------------------------------------------------------------------- /refact_lsp: -------------------------------------------------------------------------------- 1 | main -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "refact" -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /almost-all-features-05x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smallcloudai/refact-intellij/HEAD/almost-all-features-05x.jpg -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smallcloudai/refact-intellij/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/struct/Exceptions.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.struct 2 | 3 | class SMCExceptions(msg: String? = null) : Exception(msg) -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/struct/DeploymentMode.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.struct 2 | 3 | enum class DeploymentMode { 4 | CLOUD, SELF_HOSTED, HF 5 | } -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/statistic/UsageStatistic.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.statistic 2 | 3 | data class UsageStatistic(val scope: String = "", val subScope: String = "", val extension: String = "") 4 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/utils/getExtension.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.utils 2 | 3 | fun getExtension(fileName: String): String { 4 | val dotIndex = fileName.lastIndexOf(".") 5 | return fileName.substring(dotIndex + 1) 6 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/struct/ChatMessage.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.struct 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class ChatMessage(val role: String, 6 | val content: String, 7 | @SerializedName("tool_call_id") val toolCallId: String?, 8 | val usage: String?) -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/modes/diff/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.modes.diff 2 | 3 | import com.intellij.openapi.editor.Editor 4 | import com.intellij.openapi.editor.LogicalPosition 5 | 6 | fun getOffsetFromStringNumber(editor: Editor, stringNumber: Int, column: Int = 0): Int { 7 | return editor.logicalPositionToOffset(LogicalPosition(maxOf(stringNumber, 0), column)) 8 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/smallcloud/refactai/panes/sharedchat/ChatWebViewTestSuite.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.panes.sharedchat 2 | 3 | import org.junit.runner.RunWith 4 | import org.junit.runners.Suite 5 | 6 | @RunWith(Suite::class) 7 | @Suite.SuiteClasses( 8 | WorkingValidationTest::class, 9 | BasicChatWebViewTest::class 10 | ) 11 | class ChatWebViewTestSuite { 12 | companion object 13 | } 14 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/utils/LastProjectGetter.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.utils 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.project.ProjectManager 5 | import com.intellij.openapi.wm.IdeFocusManager 6 | 7 | fun getLastUsedProject(): Project { 8 | return IdeFocusManager.getGlobalInstance().lastFocusedFrame?.project 9 | ?: ProjectManager.getInstance().openProjects.firstOrNull() 10 | ?: ProjectManager.getInstance().defaultProject 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/modes/completion/structs/Completion.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.modes.completion.structs 2 | 3 | 4 | data class Completion( 5 | val originalText: String, 6 | var completion: String = "", 7 | val multiline: Boolean, 8 | val offset: Int, 9 | val createdTs: Double = -1.0, 10 | val isFromCache: Boolean = false, 11 | var snippetTelemetryId: Int? = null 12 | ) { 13 | fun updateCompletion(text: String) { 14 | completion += text 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/listeners/PluginListener.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.listeners 2 | 3 | import com.intellij.ide.plugins.DynamicPluginListener 4 | import com.intellij.ide.plugins.IdeaPluginDescriptor 5 | import com.intellij.openapi.Disposable 6 | 7 | class PluginListener: DynamicPluginListener, Disposable { 8 | override fun beforePluginUnload(pluginDescriptor: IdeaPluginDescriptor, isUpdate: Boolean) { 9 | // Disposer.dispose(PluginState.instance) 10 | } 11 | 12 | override fun dispose() {} 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/RefactAIBundle.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai 2 | 3 | import com.intellij.DynamicBundle 4 | import org.jetbrains.annotations.Nls 5 | import org.jetbrains.annotations.PropertyKey 6 | 7 | private const val BUNDLE = "bundles.RefactAI" 8 | 9 | object RefactAIBundle : DynamicBundle(BUNDLE) { 10 | private val INSTANCE: RefactAIBundle = RefactAIBundle 11 | 12 | @Nls 13 | fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any): String { 14 | return INSTANCE.getMessage(key, *params) 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/io/Connection.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.io 2 | 3 | 4 | import com.intellij.util.messages.Topic 5 | 6 | interface ConnectionChangedNotifier { 7 | fun statusChanged(newStatus: ConnectionStatus) {} 8 | fun lastErrorMsgChanged(newMsg: String?) {} 9 | 10 | companion object { 11 | val TOPIC = Topic.create( 12 | "Connection Changed Notifier", 13 | ConnectionChangedNotifier::class.java 14 | ) 15 | } 16 | } 17 | 18 | enum class ConnectionStatus { 19 | CONNECTED, 20 | PENDING, 21 | DISCONNECTED, 22 | ERROR 23 | } 24 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/listeners/InlineActionPromoter.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.listeners 2 | 3 | import com.intellij.openapi.actionSystem.ActionPromoter 4 | import com.intellij.openapi.actionSystem.AnAction 5 | import com.intellij.openapi.actionSystem.DataContext 6 | 7 | class InlineActionsPromoter : ActionPromoter { 8 | override fun promote(actions: MutableList, context: DataContext): MutableList { 9 | // if (!InferenceGlobalContext.useForceCompletion) return actions.toMutableList() 10 | return actions.filterIsInstance().toMutableList() 11 | } 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/listeners/GlobalCaretListener.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.listeners 2 | 3 | import com.intellij.openapi.diagnostic.Logger 4 | import com.intellij.openapi.editor.event.CaretEvent 5 | import com.intellij.openapi.editor.event.CaretListener 6 | import com.smallcloud.refactai.modes.ModeProvider 7 | 8 | class GlobalCaretListener : CaretListener { 9 | override fun caretPositionChanged(event: CaretEvent) { 10 | Logger.getInstance("CaretListener").debug("caretPositionChanged") 11 | val provider = ModeProvider.getOrCreateModeProvider(event.editor) 12 | provider.onCaretChange(event) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/account/AccountManagerChangedNotifier.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.account 2 | 3 | import com.intellij.util.messages.Topic 4 | 5 | interface AccountManagerChangedNotifier { 6 | 7 | fun isLoggedInChanged(isLoggedIn: Boolean) {} 8 | fun planStatusChanged(newPlan: String?) {} 9 | fun userChanged(newUser: String?) {} 10 | fun apiKeyChanged(newApiKey: String?) {} 11 | fun meteringBalanceChanged(newBalance: Int?) {} 12 | 13 | 14 | companion object { 15 | val TOPIC = Topic.create("Account Manager Changed Notifier", 16 | AccountManagerChangedNotifier::class.java) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | # libraries 3 | junit = "4.13.2" 4 | 5 | # plugins 6 | changelog = "2.2.1" 7 | intelliJPlatform = "2.5.0" 8 | kotlin = "1.9.25" 9 | kover = "0.8.3" 10 | qodana = "2024.2.3" 11 | 12 | [libraries] 13 | junit = { group = "junit", name = "junit", version.ref = "junit" } 14 | 15 | [plugins] 16 | changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } 17 | intelliJPlatform = { id = "org.jetbrains.intellij.platform", version.ref = "intelliJPlatform" } 18 | kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 19 | kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } 20 | qodana = { id = "org.jetbrains.qodana", version.ref = "qodana" } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea 9 | *.iws 10 | *.iml 11 | *.ipr 12 | out/ 13 | !**/src/main/**/out/ 14 | !**/src/test/**/out/ 15 | 16 | ### Eclipse ### 17 | .apt_generated 18 | .classpath 19 | .factorypath 20 | .project 21 | .settings 22 | .springBeans 23 | .sts4-cache 24 | bin/ 25 | !**/src/main/**/bin/ 26 | !**/src/test/**/bin/ 27 | 28 | ### NetBeans ### 29 | /nbproject/private/ 30 | /nbbuild/ 31 | /dist/ 32 | .intellijPlatform/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | 39 | ### Mac OS ### 40 | .DS_Store 41 | 42 | src/main/resources/bin/ 43 | src/main/resources/webview/dist -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/modes/completion/structs/DocumentEventExtra.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.modes.completion.structs 2 | 3 | import com.intellij.openapi.editor.Editor 4 | import com.intellij.openapi.editor.event.DocumentEvent 5 | import java.lang.System.currentTimeMillis 6 | 7 | data class DocumentEventExtra( 8 | val event: DocumentEvent?, 9 | val editor: Editor, 10 | val ts: Long, 11 | val force: Boolean = false, 12 | val offsetCorrection: Int = 0 13 | ) { 14 | companion object { 15 | fun empty(editor: Editor): DocumentEventExtra { 16 | return DocumentEventExtra( 17 | null, editor, currentTimeMillis() 18 | ) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/struct/SMCPrediction.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.struct 2 | 3 | import com.google.gson.annotations.Expose 4 | import com.google.gson.annotations.SerializedName 5 | 6 | data class SMCStreamingPeace( 7 | val choices: List, 8 | val created: Double, 9 | val model: String, 10 | @SerializedName("snippet_telemetry_id") val snippetTelemetryId: Int? = null, 11 | val cached: Boolean = false, 12 | @Expose 13 | var requestId: String = "" 14 | ) 15 | 16 | 17 | data class StreamingChoice( 18 | val index: Int, 19 | @SerializedName("code_completion") val delta: String, 20 | @SerializedName("finish_reason") val finishReason: String?, 21 | ) 22 | 23 | data class HeadMidTail( 24 | var head: Int, 25 | var mid: String, 26 | val tail: Int 27 | ) 28 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/listeners/GlobalFocusListener.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.listeners 2 | 3 | import com.intellij.openapi.diagnostic.Logger 4 | import com.intellij.openapi.editor.Editor 5 | import com.intellij.openapi.editor.ex.FocusChangeListener 6 | import com.smallcloud.refactai.modes.ModeProvider 7 | 8 | class GlobalFocusListener : FocusChangeListener { 9 | override fun focusGained(editor: Editor) { 10 | Logger.getInstance("FocusListener").debug("focusGained") 11 | val provider = ModeProvider.getOrCreateModeProvider(editor) 12 | provider.focusGained() 13 | } 14 | 15 | override fun focusLost(editor: Editor) { 16 | Logger.getInstance("FocusListener").debug("focusLost") 17 | val provider = ModeProvider.getOrCreateModeProvider(editor) 18 | provider.focusLost() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/listeners/CancelActionPromoter.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.listeners 2 | 3 | import com.intellij.openapi.actionSystem.ActionPromoter 4 | import com.intellij.openapi.actionSystem.AnAction 5 | import com.intellij.openapi.actionSystem.CommonDataKeys 6 | import com.intellij.openapi.actionSystem.DataContext 7 | import com.intellij.openapi.editor.Editor 8 | 9 | class CancelActionsPromoter : ActionPromoter { 10 | private fun getEditor(dataContext: DataContext): Editor? { 11 | return CommonDataKeys.EDITOR.getData(dataContext) 12 | } 13 | override fun promote(actions: MutableList, context: DataContext): MutableList { 14 | if (getEditor(context) == null) 15 | return actions.toMutableList() 16 | return actions.filterIsInstance().toMutableList() 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/resources/webview/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Refact.ai 6 | 7 | 8 | 27 | 28 | 29 |
30 | 31 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/modes/Mode.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.modes 2 | 3 | 4 | import com.intellij.openapi.actionSystem.DataContext 5 | import com.intellij.openapi.editor.Caret 6 | import com.intellij.openapi.editor.Editor 7 | import com.intellij.openapi.editor.event.CaretEvent 8 | import com.smallcloud.refactai.modes.completion.structs.DocumentEventExtra 9 | 10 | interface Mode { 11 | var needToRender: Boolean 12 | fun beforeDocumentChangeNonBulk(event: DocumentEventExtra) 13 | fun onTextChange(event: DocumentEventExtra) 14 | fun onTabPressed(editor: Editor, caret: Caret?, dataContext: DataContext) 15 | fun onEscPressed(editor: Editor, caret: Caret?, dataContext: DataContext) 16 | fun onCaretChange(event: CaretEvent) 17 | fun isInActiveState(): Boolean 18 | fun show() 19 | fun hide() 20 | fun cleanup(editor: Editor) 21 | } 22 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/listeners/ForceCompletionActionPromoter.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.listeners 2 | 3 | import com.intellij.codeInsight.inline.completion.session.InlineCompletionContext 4 | import com.intellij.openapi.actionSystem.ActionPromoter 5 | import com.intellij.openapi.actionSystem.AnAction 6 | import com.intellij.openapi.actionSystem.CommonDataKeys 7 | import com.intellij.openapi.actionSystem.DataContext 8 | 9 | class ForceCompletionActionPromoter : ActionPromoter { 10 | override fun promote(actions: MutableList, context: DataContext): List { 11 | val editor = CommonDataKeys.EDITOR.getData(context) ?: return emptyList() 12 | 13 | if (InlineCompletionContext.getOrNull(editor) == null) { 14 | return emptyList() 15 | } 16 | return actions.filterIsInstance() 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/notifications/Initializer.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.notifications 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.smallcloud.refactai.ExtraInfoChangedNotifier 5 | import com.smallcloud.refactai.PluginState 6 | 7 | fun notificationStartup() { 8 | ApplicationManager.getApplication() 9 | .messageBus 10 | .connect(PluginState.instance) 11 | .subscribe(ExtraInfoChangedNotifier.TOPIC, object : ExtraInfoChangedNotifier { 12 | override fun loginMessageChanged(newMsg: String?) { 13 | if (newMsg != null) 14 | emitInfo(newMsg) 15 | } 16 | override fun inferenceMessageChanged(newMsg: String?) { 17 | if (newMsg != null) 18 | emitInfo(newMsg) 19 | } 20 | }) 21 | startup() 22 | } 23 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/ChatPaneInvokeActionPromoter.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.panes.sharedchat 2 | 3 | import com.intellij.openapi.actionSystem.ActionPromoter 4 | import com.intellij.openapi.actionSystem.AnAction 5 | import com.intellij.openapi.actionSystem.CommonDataKeys 6 | import com.intellij.openapi.actionSystem.DataContext 7 | import com.intellij.openapi.editor.Editor 8 | 9 | class ChatPaneInvokeActionPromoter : ActionPromoter { 10 | private fun getEditor(dataContext: DataContext): Editor? { 11 | return CommonDataKeys.EDITOR.getData(dataContext) 12 | } 13 | 14 | override fun promote(actions: MutableList, context: DataContext): MutableList { 15 | if (getEditor(context) == null) 16 | return actions.toMutableList() 17 | return actions.filterIsInstance().toMutableList() 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/utils/LinksPanel.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.utils 2 | 3 | import com.intellij.icons.AllIcons 4 | import com.intellij.ide.BrowserUtil 5 | import com.intellij.ui.components.labels.LinkLabel 6 | import org.jdesktop.swingx.HorizontalLayout 7 | import javax.swing.JPanel 8 | 9 | fun makeLinksPanel(): JPanel { 10 | return JPanel(HorizontalLayout()).apply { 11 | add(LinkLabel("Bug report", AllIcons.Ide.External_link_arrow).apply { 12 | 13 | setListener({ _, _ -> 14 | BrowserUtil.browse("https://github.com/smallcloudai/refact-intellij/issues") 15 | }, null) 16 | }) 17 | add(LinkLabel("Discord", AllIcons.Ide.External_link_arrow).apply { 18 | setListener({ _, _ -> 19 | BrowserUtil.browse("https://www.smallcloud.ai/discord") 20 | }, null) 21 | }) 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/resources/icons/hand_12x12.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/listeners/AcceptActionPromoter.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.listeners 2 | 3 | import com.intellij.codeInsight.inline.completion.session.InlineCompletionContext 4 | import com.intellij.openapi.actionSystem.ActionPromoter 5 | import com.intellij.openapi.actionSystem.AnAction 6 | import com.intellij.openapi.actionSystem.CommonDataKeys 7 | import com.intellij.openapi.actionSystem.DataContext 8 | import com.intellij.openapi.editor.Editor 9 | 10 | class AcceptActionsPromoter : ActionPromoter { 11 | private fun getEditor(dataContext: DataContext): Editor? { 12 | return CommonDataKeys.EDITOR.getData(dataContext) 13 | } 14 | override fun promote(actions: MutableList, context: DataContext): List { 15 | val editor = getEditor(context) ?: return emptyList() 16 | if (InlineCompletionContext.getOrNull(editor) == null) { 17 | return emptyList() 18 | } 19 | actions.filterIsInstance().takeIf { it.isNotEmpty() }?.let { return it } 20 | return emptyList() 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/modes/completion/CompletionTracker.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.modes.completion 2 | 3 | import com.intellij.openapi.editor.Editor 4 | import com.intellij.openapi.util.Key 5 | 6 | object CompletionTracker { 7 | private val LAST_COMPLETION_REQUEST_TIME = Key.create("LAST_COMPLETION_REQUEST_TIME") 8 | private const val DEBOUNCE_INTERVAL_MS = 500 9 | 10 | fun calcDebounceTime(editor: Editor): Long { 11 | val lastCompletionTimestamp = LAST_COMPLETION_REQUEST_TIME[editor] 12 | if (lastCompletionTimestamp != null) { 13 | val elapsedTimeFromLastEvent = System.currentTimeMillis() - lastCompletionTimestamp 14 | if (elapsedTimeFromLastEvent < DEBOUNCE_INTERVAL_MS) { 15 | return DEBOUNCE_INTERVAL_MS - elapsedTimeFromLastEvent 16 | } 17 | } 18 | return 0 19 | } 20 | 21 | fun updateLastCompletionRequestTime(editor: Editor) { 22 | val currentTimestamp = System.currentTimeMillis() 23 | LAST_COMPLETION_REQUEST_TIME[editor] = currentTimestamp 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/lsp/LSPActiveDocNotifierService.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.lsp 2 | 3 | import com.intellij.openapi.Disposable 4 | import com.intellij.openapi.application.ApplicationManager 5 | import com.intellij.openapi.editor.Editor 6 | import com.intellij.openapi.project.Project 7 | import com.smallcloud.refactai.listeners.LastEditorGetterListener 8 | import com.smallcloud.refactai.listeners.SelectionChangedNotifier 9 | 10 | class LSPActiveDocNotifierService(val project: Project): Disposable { 11 | init { 12 | if (LastEditorGetterListener.LAST_EDITOR != null) { 13 | lspSetActiveDocument(LastEditorGetterListener.LAST_EDITOR!!) 14 | } 15 | 16 | ApplicationManager.getApplication().messageBus.connect(this) 17 | .subscribe(SelectionChangedNotifier.TOPIC, object : SelectionChangedNotifier { 18 | override fun isEditorChanged(editor: Editor?) { 19 | if (editor == null) return 20 | lspSetActiveDocument(editor) 21 | } 22 | }) 23 | } 24 | 25 | override fun dispose() {} 26 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/lsp/LSPTools.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.lsp 2 | 3 | data class ToolFunctionParameters( 4 | val properties: Map>, 5 | val type: String, 6 | val required: Array 7 | ) { 8 | override fun equals(other: Any?): Boolean { 9 | if (this === other) return true 10 | if (javaClass != other?.javaClass) return false 11 | 12 | other as ToolFunctionParameters 13 | 14 | if (properties != other.properties) return false 15 | if (type != other.type) return false 16 | if (!required.contentEquals(other.required)) return false 17 | 18 | return true 19 | } 20 | 21 | override fun hashCode(): Int { 22 | var result = properties.hashCode() 23 | result = 31 * result + type.hashCode() 24 | result = 31 * result + required.contentHashCode() 25 | return result 26 | } 27 | } 28 | 29 | data class ToolFunction(val description: String, val name: String, val parameters: ToolFunctionParameters) 30 | data class Tool(val function: ToolFunction, val type: String); -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/ChatPaneInvokeAction.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.panes.sharedchat 2 | 3 | import com.intellij.openapi.actionSystem.AnAction 4 | import com.intellij.openapi.actionSystem.AnActionEvent 5 | import com.intellij.openapi.components.service 6 | import com.intellij.openapi.wm.ToolWindowManager 7 | import com.smallcloud.refactai.Resources 8 | import com.smallcloud.refactai.panes.RefactAIToolboxPaneFactory 9 | import com.smallcloud.refactai.statistic.UsageStatistic 10 | import com.smallcloud.refactai.statistic.UsageStats 11 | import com.smallcloud.refactai.utils.getLastUsedProject 12 | 13 | class ChatPaneInvokeAction: AnAction(Resources.Icons.LOGO_RED_16x16) { 14 | override fun actionPerformed(e: AnActionEvent) { 15 | actionPerformed() 16 | } 17 | 18 | fun actionPerformed() { 19 | val chat = ToolWindowManager.getInstance(getLastUsedProject()).getToolWindow("Refact") 20 | chat?.activate { 21 | RefactAIToolboxPaneFactory.focusChat() 22 | getLastUsedProject().service().addChatStatistic(true, UsageStatistic("openChatByShortcut"), "") 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/FimCache.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai 2 | 3 | import com.google.gson.Gson 4 | import com.smallcloud.refactai.panes.sharedchat.Events 5 | import kotlinx.coroutines.flow.* 6 | 7 | import kotlinx.coroutines.runBlocking 8 | 9 | 10 | object FimCache { 11 | private val _events = MutableSharedFlow(); 12 | val events = _events.asSharedFlow(); 13 | 14 | suspend fun subscribe(block: (Events.Fim.FimDebugPayload) -> Unit) { 15 | events.filterIsInstance().collectLatest { 16 | block(it) 17 | } 18 | } 19 | 20 | 21 | fun emit(data: Events.Fim.FimDebugPayload) { 22 | runBlocking { 23 | _events.emit(data) 24 | } 25 | } 26 | 27 | var last: Events.Fim.FimDebugPayload? = null 28 | 29 | fun maybeSendFimData(res: String) { 30 | // println("FimCache.maybeSendFimData: $res") 31 | try { 32 | val data = Gson().fromJson(res, Events.Fim.FimDebugPayload::class.java); 33 | last = data; 34 | emit(data); 35 | } catch (e: Exception) { 36 | // ignore 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/modes/completion/StubCompletionMode.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.modes.completion 2 | 3 | import com.intellij.openapi.actionSystem.DataContext 4 | import com.intellij.openapi.editor.Caret 5 | import com.intellij.openapi.editor.Editor 6 | import com.intellij.openapi.editor.event.CaretEvent 7 | import com.smallcloud.refactai.modes.Mode 8 | import com.smallcloud.refactai.modes.completion.structs.DocumentEventExtra 9 | 10 | 11 | class StubCompletionMode( 12 | override var needToRender: Boolean = true 13 | ) : Mode { 14 | override fun beforeDocumentChangeNonBulk(event: DocumentEventExtra) {} 15 | 16 | override fun onTextChange(event: DocumentEventExtra) {} 17 | 18 | override fun onTabPressed(editor: Editor, caret: Caret?, dataContext: DataContext) {} 19 | 20 | override fun onEscPressed(editor: Editor, caret: Caret?, dataContext: DataContext) {} 21 | 22 | override fun onCaretChange(event: CaretEvent) {} 23 | 24 | override fun isInActiveState(): Boolean { 25 | return false 26 | } 27 | 28 | override fun show() {} 29 | 30 | override fun hide() {} 31 | 32 | override fun cleanup(editor: Editor) {} 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/modes/diff/renderer/RenderHelper.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.modes.diff.renderer 2 | 3 | import com.intellij.openapi.editor.Editor 4 | import com.intellij.openapi.editor.colors.EditorFontType 5 | import com.intellij.ui.JBColor 6 | import java.awt.Color 7 | import java.awt.Font 8 | import java.awt.font.TextAttribute 9 | 10 | 11 | val greenColor = Color(0, 200, 0, 50) 12 | val redColor = Color(200, 0, 0, 50) 13 | val veryGreenColor = Color(0, 200, 0, 100) 14 | val veryRedColor = Color(200, 0, 0, 100) 15 | 16 | object RenderHelper { 17 | fun getFont(editor: Editor, deprecated: Boolean): Font { 18 | val font = editor.colorsScheme.getFont(EditorFontType.PLAIN) 19 | if (!deprecated) { 20 | return font 21 | } 22 | val attributes: MutableMap = HashMap(font.attributes) 23 | attributes[TextAttribute.STRIKETHROUGH] = TextAttribute.STRIKETHROUGH_ON 24 | return Font(attributes) 25 | } 26 | 27 | val color: Color 28 | get() { 29 | return JBColor.GRAY 30 | } 31 | 32 | val underlineColor: Color 33 | get() { 34 | return JBColor.BLUE 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/status_bar/StatusBarProvider.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.status_bar 2 | 3 | import com.intellij.openapi.Disposable 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.util.Disposer 6 | import com.intellij.openapi.wm.StatusBar 7 | import com.intellij.openapi.wm.StatusBarWidget 8 | import com.intellij.openapi.wm.StatusBarWidgetFactory 9 | import com.smallcloud.refactai.Resources 10 | import org.jetbrains.annotations.Nullable 11 | 12 | class SMCStatusBarWidgetFactory : StatusBarWidgetFactory, Disposable { 13 | override fun getId(): String { 14 | return "SMCStatusBarWidgetFactory" 15 | } 16 | 17 | override fun getDisplayName(): String { 18 | return Resources.titleStr 19 | } 20 | 21 | override fun isAvailable(project: Project): Boolean { 22 | return true 23 | } 24 | 25 | @Nullable 26 | override fun createWidget(project: Project): StatusBarWidget { 27 | return SMCStatusBarWidget(project) 28 | } 29 | 30 | override fun disposeWidget(widget: StatusBarWidget) { 31 | Disposer.dispose(widget) 32 | } 33 | 34 | override fun canBeEnabledOn(statusBar: StatusBar): Boolean { 35 | return true 36 | } 37 | 38 | override fun dispose() {} 39 | } 40 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/code_lens/CodeLensInvalidatorService.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.code_lens 2 | 3 | import com.intellij.codeInsight.codeVision.CodeVisionHost 4 | import com.intellij.openapi.Disposable 5 | import com.intellij.openapi.application.invokeLater 6 | import com.intellij.openapi.components.service 7 | import com.intellij.openapi.diagnostic.logger 8 | import com.intellij.openapi.project.Project 9 | import com.smallcloud.refactai.lsp.LSPProcessHolderChangedNotifier 10 | 11 | class CodeLensInvalidatorService(project: Project): Disposable { 12 | private var ids: List = emptyList() 13 | override fun dispose() {} 14 | fun setCodeLensIds(ids: List) { 15 | this.ids = ids 16 | } 17 | 18 | init { 19 | project.messageBus.connect(this).subscribe(LSPProcessHolderChangedNotifier.TOPIC, object : LSPProcessHolderChangedNotifier { 20 | override fun lspIsActive(isActive: Boolean) { 21 | invokeLater { 22 | logger().warn("Invalidating code lens") 23 | project.service() 24 | .invalidateProvider(CodeVisionHost.LensInvalidateSignal(null, ids)) 25 | } 26 | } 27 | }) 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/io/Fetch.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.io 2 | 3 | import java.net.HttpURLConnection 4 | import java.net.URI 5 | 6 | 7 | data class Response( 8 | val statusCode: Int, 9 | val headers: Map>? = null, 10 | val body: String? = null 11 | ) 12 | 13 | 14 | fun sendRequest( 15 | uri: URI, method: String = "GET", 16 | headers: Map? = null, 17 | body: String? = null, 18 | requestProperties: Map? = null 19 | ): Response { 20 | val conn = uri.toURL().openConnection() as HttpURLConnection 21 | 22 | requestProperties?.forEach { 23 | conn.setRequestProperty(it.key, it.value) 24 | } 25 | 26 | with(conn) { 27 | requestMethod = method 28 | doOutput = body != null 29 | headers?.forEach(this::setRequestProperty) 30 | } 31 | 32 | if (body != null) { 33 | conn.outputStream.use { 34 | it.write(body.toByteArray()) 35 | } 36 | } 37 | val responseBody = if (conn.responseCode in 100..399) { 38 | conn.inputStream.use { it.readBytes() }.toString(Charsets.UTF_8) 39 | } else { 40 | conn.errorStream.use { it.readBytes() }.toString(Charsets.UTF_8) 41 | } 42 | return Response(conn.responseCode, conn.headerFields, responseBody) 43 | } 44 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/listeners/UninstallListener.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.listeners 2 | 3 | import com.intellij.ide.BrowserUtil 4 | import com.intellij.ide.plugins.IdeaPluginDescriptor 5 | import com.intellij.ide.plugins.PluginStateListener 6 | import com.smallcloud.refactai.Resources 7 | import com.smallcloud.refactai.Resources.defaultCloudUrl 8 | import com.smallcloud.refactai.statistic.UsageStatistic 9 | import com.smallcloud.refactai.account.AccountManager.Companion.instance as AccountManager 10 | import com.smallcloud.refactai.statistic.UsageStats.Companion.instance as UsageStats 11 | 12 | private var SINGLE_TIME_UNINSTALL = 0 13 | 14 | class UninstallListener: PluginStateListener { 15 | override fun install(descriptor: IdeaPluginDescriptor) {} 16 | 17 | override fun uninstall(descriptor: IdeaPluginDescriptor) { 18 | if (descriptor.pluginId != Resources.pluginId) { 19 | return 20 | } 21 | 22 | if (Thread.currentThread().stackTrace.any { it.methodName == "uninstallAndUpdateUi" } 23 | && SINGLE_TIME_UNINSTALL == 0) { 24 | SINGLE_TIME_UNINSTALL++ 25 | UsageStats?.addStatistic(true, UsageStatistic("uninstall"), defaultCloudUrl.toString(), "") 26 | BrowserUtil.browse("https://refact.ai/feedback?ide=${Resources.client}&tenant=${AccountManager.user}") 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/codecompletion/RefactInlineCompletionDocumentListener.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.codecompletion 2 | 3 | import com.intellij.codeInsight.inline.completion.InlineCompletion 4 | import com.intellij.openapi.application.ApplicationManager 5 | import com.intellij.openapi.editor.Document 6 | import com.intellij.openapi.editor.Editor 7 | import com.intellij.openapi.editor.EditorFactory 8 | import com.intellij.openapi.editor.event.BulkAwareDocumentListener 9 | import com.intellij.openapi.editor.event.DocumentEvent 10 | import com.intellij.util.application 11 | 12 | class RefactInlineCompletionDocumentListener : BulkAwareDocumentListener { 13 | override fun documentChangedNonBulk(event: DocumentEvent) { 14 | val editor = getActiveEditor(event.document) ?: return 15 | val handler = InlineCompletion.getHandlerOrNull(editor) 16 | application.invokeLater { 17 | handler?.invokeEvent( 18 | RefactAIContinuousEvent( 19 | editor, editor.caretModel.offset 20 | ) 21 | ) 22 | } 23 | } 24 | 25 | 26 | private fun getActiveEditor(document: Document): Editor? { 27 | if (!ApplicationManager.getApplication().isDispatchThread) { 28 | return null 29 | } 30 | return EditorFactory.getInstance().getEditors(document).firstOrNull() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/io/InferenceGlobalContextChangedNotifier.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.io 2 | 3 | import com.intellij.util.messages.Topic 4 | import com.smallcloud.refactai.struct.DeploymentMode 5 | import java.net.URI 6 | 7 | interface InferenceGlobalContextChangedNotifier { 8 | fun inferenceUriChanged(newUrl: URI?) {} 9 | fun userInferenceUriChanged(newUrl: String?) {} 10 | fun temperatureChanged(newTemp: Float?) {} 11 | fun modelChanged(newModel: String?) {} 12 | fun lastAutoModelChanged(newModel: String?) {} 13 | fun useAutoCompletionModeChanged(newValue: Boolean) {} 14 | fun developerModeEnabledChanged(newValue: Boolean) {} 15 | fun deploymentModeChanged(newMode: DeploymentMode) {} 16 | fun astFlagChanged(newValue: Boolean) {} 17 | fun astFileLimitChanged(newValue: Int) {} 18 | fun vecdbFlagChanged(newValue: Boolean) {} 19 | fun vecdbFileLimitChanged(newValue: Int) {} 20 | fun xDebugLSPPortChanged(newPort: Int?) {} 21 | fun insecureSSLChanged(newValue: Boolean) {} 22 | fun completionMaxTokensChanged(newMaxTokens: Int) {} 23 | fun telemetrySnippetsEnabledChanged(newValue: Boolean) {} 24 | fun experimentalLspFlagEnabledChanged(newValue: Boolean) {} 25 | 26 | companion object { 27 | val TOPIC = Topic.create( 28 | "Inference Global Context Changed Notifier", 29 | InferenceGlobalContextChangedNotifier::class.java 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/kotlin/com/smallcloud/refactai/lsp/LspToolsTest.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.lsp 2 | 3 | import com.google.gson.Gson 4 | import org.junit.Assert.* 5 | import kotlin.test.Test 6 | 7 | class LspToolsTest { 8 | @Test 9 | fun parseResponse() { 10 | val query = """{"description": "Single line, paragraph or code sample.", "type": "string"}""" 11 | val parameters = """{"properties": {"query":$query}, "required": ["query"], "type": "object"}""" 12 | val tool = """{"description": "Find similar pieces of code using vector database","name": "workspace","parameters":$parameters}""" 13 | val res = """[{"function": $tool,"type": "function"}]""" 14 | 15 | val expectedParameters = ToolFunctionParameters( 16 | properties = mapOf("query" to mapOf("description" to "Single line, paragraph or code sample.", "type" to "string")), 17 | type = "object", 18 | required = arrayOf("query") 19 | ) 20 | 21 | val expectFunction = ToolFunction( 22 | description = "Find similar pieces of code using vector database", 23 | name = "workspace", 24 | parameters = expectedParameters 25 | ) 26 | val expectedTool = Tool(function = expectFunction, type="function") 27 | 28 | print(res) 29 | 30 | val result = Gson().fromJson(res, Array::class.java) 31 | 32 | assertEquals(expectedTool, result.first()) 33 | 34 | 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/resources/icons/refactai_logo_red_12x12.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/panes/sharedchat/ChatPanes.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.panes.sharedchat 2 | 3 | import com.intellij.openapi.Disposable 4 | import com.intellij.openapi.application.invokeLater 5 | import com.intellij.openapi.project.Project 6 | import com.smallcloud.refactai.struct.ChatMessage 7 | import java.awt.BorderLayout 8 | import javax.swing.JComponent 9 | import javax.swing.JPanel 10 | 11 | class ChatPanes(val project: Project) : Disposable { 12 | private var component: JComponent? = null 13 | private var pane: SharedChatPane? = null 14 | private val holder = JPanel().also { 15 | it.layout = BorderLayout() 16 | } 17 | 18 | private fun setupPanes() { 19 | invokeLater { 20 | holder.removeAll() 21 | pane = SharedChatPane(project) 22 | component = pane?.webView?.component 23 | holder.add(component) 24 | } 25 | } 26 | 27 | init { 28 | setupPanes() 29 | } 30 | 31 | fun getComponent(): JComponent { 32 | return holder 33 | } 34 | 35 | fun executeCodeLensCommand(messages: Array, sendImmediately: Boolean, openNewTab: Boolean) { 36 | pane?.executeCodeLensCommand(messages, sendImmediately, openNewTab) 37 | } 38 | 39 | fun requestFocus() { 40 | component?.requestFocus() 41 | } 42 | 43 | fun newChat() { 44 | pane?.newChat() 45 | } 46 | 47 | override fun dispose() { 48 | pane?.dispose() 49 | } 50 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/lsp/RagStatus.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.lsp 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | 6 | data class RagStatus( 7 | @SerializedName("ast") val ast: AstStatus? = null, 8 | @SerializedName("ast_alive") val astAlive: String? = null, 9 | @SerializedName("vecdb") val vecdb: VecDbStatus? = null, 10 | @SerializedName("vecdb_alive") val vecdbAlive: String? = null, 11 | @SerializedName("vec_db_error") val vecDbError: String 12 | ) 13 | 14 | data class AstStatus( 15 | @SerializedName("files_unparsed") val filesUnparsed: Int, 16 | @SerializedName("files_total") val filesTotal: Int, 17 | @SerializedName("ast_index_files_total") val astIndexFilesTotal: Int, 18 | @SerializedName("ast_index_symbols_total") val astIndexSymbolsTotal: Int, 19 | @SerializedName("state") val state: String, 20 | @SerializedName("ast_max_files_hit") val astMaxFilesHit: Boolean 21 | ) 22 | 23 | data class VecDbStatus( 24 | @SerializedName("files_unprocessed") val filesUnprocessed: Int, 25 | @SerializedName("files_total") val filesTotal: Int, 26 | @SerializedName("requests_made_since_start") val requestsMadeSinceStart: Int, 27 | @SerializedName("vectors_made_since_start") val vectorsMadeSinceStart: Int, 28 | @SerializedName("db_size") val dbSize: Int, 29 | @SerializedName("db_cache_size") val dbCacheSize: Int, 30 | @SerializedName("state") val state: String, 31 | @SerializedName("vecdb_max_files_hit") val vecdbMaxFilesHit: Boolean 32 | ) -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/listeners/LSPDocumentListener.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.listeners 2 | 3 | import com.intellij.openapi.Disposable 4 | import com.intellij.openapi.application.ApplicationManager 5 | import com.intellij.openapi.editor.Document 6 | import com.intellij.openapi.editor.Editor 7 | import com.intellij.openapi.editor.EditorFactory 8 | import com.intellij.openapi.editor.event.BulkAwareDocumentListener 9 | import com.intellij.openapi.editor.event.DocumentEvent 10 | import com.intellij.openapi.fileEditor.FileDocumentManager 11 | import com.intellij.openapi.vfs.VirtualFile 12 | import com.smallcloud.refactai.lsp.lspDocumentDidChanged 13 | 14 | 15 | class LSPDocumentListener : BulkAwareDocumentListener, Disposable { 16 | override fun documentChanged(event: DocumentEvent) { 17 | val editor = getActiveEditor(event.document) ?: return 18 | val vFile = getVirtualFile(editor) ?: return 19 | if (!vFile.exists()) return 20 | val project = editor.project!! 21 | 22 | lspDocumentDidChanged(project, vFile.url, editor.document.text) 23 | } 24 | 25 | private fun getActiveEditor(document: Document): Editor? { 26 | if (!ApplicationManager.getApplication().isDispatchThread) { 27 | return null 28 | } 29 | return EditorFactory.getInstance().getEditors(document).firstOrNull() 30 | } 31 | 32 | private fun getVirtualFile(editor: Editor): VirtualFile? { 33 | return FileDocumentManager.getInstance().getFile(editor.document) 34 | } 35 | 36 | override fun dispose() {} 37 | } 38 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/listeners/CancelAction.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.listeners 2 | 3 | 4 | import com.intellij.codeInsight.hint.HintManagerImpl.ActionToIgnore 5 | import com.intellij.openapi.actionSystem.DataContext 6 | import com.intellij.openapi.diagnostic.Logger 7 | import com.intellij.openapi.editor.Caret 8 | import com.intellij.openapi.editor.Editor 9 | import com.intellij.openapi.editor.actionSystem.EditorAction 10 | import com.intellij.openapi.editor.actionSystem.EditorWriteActionHandler 11 | import com.smallcloud.refactai.Resources 12 | import com.smallcloud.refactai.modes.ModeProvider 13 | 14 | class CancelPressedAction : 15 | EditorAction(InlineCompletionHandler()), 16 | ActionToIgnore { 17 | val ACTION_ID = "CancelPressedAction" 18 | 19 | init { 20 | this.templatePresentation.icon = Resources.Icons.LOGO_RED_16x16 21 | } 22 | 23 | class InlineCompletionHandler : EditorWriteActionHandler() { 24 | override fun executeWriteAction(editor: Editor, caret: Caret?, dataContext: DataContext) { 25 | Logger.getInstance("CancelPressedAction").debug("executeWriteAction") 26 | val provider = ModeProvider.getOrCreateModeProvider(editor) 27 | provider.onEscPressed(editor, caret, dataContext) 28 | } 29 | 30 | override fun isEnabledForCaret( 31 | editor: Editor, 32 | caret: Caret, 33 | dataContext: DataContext 34 | ): Boolean { 35 | return ModeProvider.getOrCreateModeProvider(editor).modeInActiveState() 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/resources/icons/refactai_logo_12x12.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, Small Magellanic Cloud AI Ltd. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /src/main/resources/icons/refactai_logo_red_13x13.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/listeners/ForceCompletionAction.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.listeners 2 | 3 | 4 | import com.intellij.codeInsight.hint.HintManagerImpl.ActionToIgnore 5 | import com.intellij.codeInsight.inline.completion.InlineCompletion 6 | import com.intellij.codeInsight.inline.completion.InlineCompletionEvent 7 | import com.intellij.openapi.actionSystem.DataContext 8 | import com.intellij.openapi.editor.Caret 9 | import com.intellij.openapi.editor.Editor 10 | import com.intellij.openapi.editor.actionSystem.EditorAction 11 | import com.intellij.openapi.editor.actionSystem.EditorWriteActionHandler 12 | import com.smallcloud.refactai.Resources 13 | 14 | 15 | // copy code from https://github.com/JetBrains/intellij-community/blob/97f1fa8169ce800fd5bfecccb07ccc869d827a4c/platform/platform-impl/src/com/intellij/codeInsight/inline/completion/InlineCompletionActions.kt#L130 16 | // CallInlineCompletionHandler became internal 17 | class CallInlineCompletionHandler : EditorWriteActionHandler() { 18 | override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { 19 | val curCaret = caret ?: editor.caretModel.currentCaret 20 | 21 | val listener = InlineCompletion.getHandlerOrNull(editor) ?: return 22 | listener.invoke(InlineCompletionEvent.DirectCall(editor, curCaret, dataContext)) 23 | } 24 | } 25 | 26 | class ForceCompletionAction : 27 | EditorAction(CallInlineCompletionHandler()), 28 | ActionToIgnore { 29 | val ACTION_ID = "ForceCompletionAction" 30 | 31 | init { 32 | this.templatePresentation.icon = Resources.Icons.LOGO_RED_16x16 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/resources/icons/refactai_logo_red_16x16.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # IntelliJ Platform Artifacts Repositories -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html 2 | 3 | pluginGroup = "com.smallcloud" 4 | pluginName = Refact.ai 5 | pluginRepositoryUrl = https://github.com/smallcloudai/refact-intellij 6 | # SemVer format -> https://semver.org 7 | pluginVersion = 6.5.2 8 | 9 | # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html 10 | pluginSinceBuild = 241 11 | pluginUntilBuild = 252.* 12 | 13 | # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension 14 | platformType = PC 15 | platformVersion = 2024.1.7 16 | #platformType = AI 17 | #platformVersion = 2023.3.1.2 18 | 19 | # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html 20 | # Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP 21 | platformPlugins = 22 | # Example: platformBundledPlugins = com.intellij.java 23 | platformBundledPlugins = 24 | 25 | # Gradle Releases -> https://github.com/gradle/gradle/releases 26 | gradleVersion = 8.10.2 27 | 28 | # Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib 29 | kotlin.stdlib.default.dependency = false 30 | 31 | # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html 32 | org.gradle.configuration-cache = true 33 | 34 | # Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html 35 | org.gradle.caching = false 36 | org.gradle.jvmargs=-Xmx2048m -------------------------------------------------------------------------------- /src/test/kotlin/com/smallcloud/refactai/io/AsyncConnectionTest.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.io 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.JsonObject 5 | import com.smallcloud.refactai.testUtils.MockServer 6 | import okhttp3.mockwebserver.MockResponse 7 | import org.junit.Test 8 | import java.net.URI 9 | import java.util.concurrent.TimeUnit 10 | 11 | 12 | 13 | class AsyncConnectionTest: MockServer() { 14 | 15 | @Test 16 | fun testBasicGetRequest() { 17 | val httpClient = AsyncConnection() 18 | // Prepare a mock response 19 | val responseBody = """{"status":"success","data":"test data"}""" 20 | this.server.enqueue( 21 | MockResponse() 22 | .setResponseCode(200) 23 | .setHeader("Content-Type", "application/json") 24 | .setBody(responseBody) 25 | ) 26 | 27 | val response = httpClient.get(URI.create(this.baseUrl + "api/test")).join().get().toString() 28 | 29 | // Verify the request was made correctly 30 | val recordedRequest = this.server.takeRequest(5, TimeUnit.SECONDS) 31 | assertNotNull(recordedRequest) 32 | assertEquals("GET", recordedRequest!!.method) 33 | assertEquals("/api/test", recordedRequest.path) 34 | 35 | // Verify the response 36 | assertEquals(responseBody, response) 37 | 38 | // Parse the JSON to verify the content 39 | val gson = Gson() 40 | val jsonObject = gson.fromJson(response, JsonObject::class.java) 41 | assertEquals("success", jsonObject.get("status").asString) 42 | assertEquals("test data", jsonObject.get("data").asString) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/modes/completion/prompt/RequestCreator.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.modes.completion.prompt 2 | 3 | import com.smallcloud.refactai.Resources 4 | import com.smallcloud.refactai.statistic.UsageStatistic 5 | import com.smallcloud.refactai.struct.SMCCursor 6 | import com.smallcloud.refactai.struct.SMCInputs 7 | import com.smallcloud.refactai.struct.SMCRequest 8 | import com.smallcloud.refactai.struct.SMCRequestBody 9 | import java.net.URI 10 | import com.smallcloud.refactai.io.InferenceGlobalContext.Companion.instance as InferenceGlobalContext 11 | 12 | object RequestCreator { 13 | fun create( 14 | fileName: String, text: String, 15 | line: Int, column: Int, 16 | stat: UsageStatistic, 17 | baseUrl: URI, 18 | model: String? = null, 19 | useAst: Boolean = false, 20 | stream: Boolean = true, 21 | multiline: Boolean = false 22 | ): SMCRequest? { 23 | val inputs = SMCInputs( 24 | sources = mutableMapOf(fileName to text), 25 | cursor = SMCCursor( 26 | file = fileName, 27 | line = line, 28 | character = column, 29 | ), 30 | multiline = multiline, 31 | ) 32 | 33 | val requestBody = SMCRequestBody( 34 | inputs = inputs, 35 | stream = stream, 36 | model = model, 37 | useAst = useAst 38 | ) 39 | 40 | return InferenceGlobalContext.makeRequest( 41 | requestBody, 42 | )?.also { 43 | it.stat = stat 44 | it.uri = baseUrl.resolve(Resources.defaultCodeCompletionUrlSuffix) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/struct/SMCRequest.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.struct 2 | 3 | import com.google.gson.annotations.SerializedName 4 | import com.smallcloud.refactai.statistic.UsageStatistic 5 | import java.net.URI 6 | import java.util.concurrent.ThreadLocalRandom 7 | import kotlin.streams.asSequence 8 | 9 | data class POI( 10 | val filename: String, 11 | val cursor0: Int, 12 | val cursor1: Int, 13 | val priority: Double, 14 | ) 15 | 16 | private val charPool : List = ('a'..'z') + ('A'..'Z') + ('0'..'9') 17 | private fun uuid() = ThreadLocalRandom.current() 18 | .ints(8.toLong(), 0, charPool.size) 19 | .asSequence() 20 | .map(charPool::get) 21 | .joinToString("") 22 | 23 | data class SMCCursor( 24 | val file: String = "", 25 | val line: Int = 0, 26 | val character: Int = 0 27 | ) 28 | data class SMCInputs( 29 | var sources: Map = mapOf(), 30 | val cursor: SMCCursor = SMCCursor(), 31 | val multiline: Boolean = true 32 | 33 | ) 34 | 35 | data class SMCParameters( 36 | var temperature: Float = 0.2f, 37 | @SerializedName("max_new_tokens") var maxNewTokens: Int = 50 38 | ) 39 | 40 | data class SMCRequestBody( 41 | var inputs: SMCInputs = SMCInputs(), 42 | var stream: Boolean = true, 43 | var parameters: SMCParameters = SMCParameters(), 44 | var model: String? = null, 45 | @SerializedName("no_cache") var noCache: Boolean = false, 46 | @SerializedName("use_ast") var useAst: Boolean = false, 47 | ) 48 | 49 | data class SMCRequest( 50 | var body: SMCRequestBody, 51 | var token: String, 52 | var uri: URI = URI(""), 53 | var id: String = uuid(), 54 | var stat: UsageStatistic = UsageStatistic(), 55 | ) 56 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/utils/OSRRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.utils 2 | 3 | import com.intellij.openapi.diagnostic.Logger 4 | import javax.swing.JComponent 5 | 6 | /** 7 | * OSR (Off-Screen Rendering) utilities for Linux systems. 8 | * Since JBCef handles OSR internally when setOffScreenRendering(true) is called, 9 | * this class provides additional optimizations and monitoring. 10 | */ 11 | class OSRRenderer( 12 | private val targetFps: Int = 30 13 | ) { 14 | private val logger = Logger.getInstance(OSRRenderer::class.java) 15 | private val frameInterval = 1000L / targetFps 16 | private var lastOptimizationTime = 0L 17 | private lateinit var hostComponent: JComponent 18 | 19 | fun attach(host: JComponent) { 20 | this.hostComponent = host 21 | logger.info("OSR optimizations attached (target ${targetFps}fps)") 22 | host.addComponentListener(object : java.awt.event.ComponentAdapter() { 23 | override fun componentResized(e: java.awt.event.ComponentEvent) { 24 | optimizeForResize() 25 | } 26 | }) 27 | } 28 | 29 | private fun optimizeForResize() { 30 | val currentTime = System.currentTimeMillis() 31 | if (currentTime - lastOptimizationTime < frameInterval) { 32 | return 33 | } 34 | lastOptimizationTime = currentTime 35 | logger.info("OSR resize optimization applied: ${hostComponent.width}x${hostComponent.height}") 36 | hostComponent.repaint() 37 | } 38 | 39 | fun cleanup() { 40 | logger.info("Cleaning up OSR renderer optimizations") 41 | lastOptimizationTime = 0L 42 | logger.info("OSR renderer cleanup completed") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/lsp/LSPCapabilities.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.lsp 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class LSPScratchpadInfo( 6 | @SerializedName("default_system_message") var defaultSystemMessage: String 7 | ) 8 | 9 | data class LSPModelInfo( 10 | @SerializedName("default_scratchpad") var defaultScratchpad: String, 11 | @SerializedName("n_ctx") var nCtx: Int, 12 | @SerializedName("similar_models") var similarModels: List, 13 | @SerializedName("supports_scratchpads") var supportsScratchpads: Map, 14 | @SerializedName("supports_stop") var supportsStop: Boolean, 15 | @SerializedName("supports_tools") var supportsTools: Boolean?, 16 | ) 17 | 18 | data class LSPCapabilities( 19 | @SerializedName("cloud_name") var cloudName: String = "", 20 | @SerializedName("code_chat_default_model") var codeChatDefaultModel: String = "", 21 | @SerializedName("code_chat_models") var codeChatModels: Map = mapOf(), 22 | @SerializedName("code_completion_default_model") var codeCompletionDefaultModel: String = "", 23 | @SerializedName("code_completion_models") var codeCompletionModels: Map = mapOf(), 24 | @SerializedName("endpoint_style") var endpointStyle: String = "", 25 | @SerializedName("endpoint_template") var endpointTemplate: String = "", 26 | @SerializedName("running_models") var runningModels: List = listOf(), 27 | @SerializedName("telemetry_basic_dest") var telemetryBasicDest: String = "", 28 | @SerializedName("tokenizer_path_template") var tokenizerPathTemplate: String = "", 29 | @SerializedName("tokenizer_rewrite_path") var tokenizerRewritePath: Map = mapOf(), 30 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/listeners/DocumentListener.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.listeners 2 | 3 | import com.intellij.openapi.Disposable 4 | import com.intellij.openapi.application.ApplicationManager 5 | import com.intellij.openapi.diagnostic.Logger 6 | import com.intellij.openapi.editor.Document 7 | import com.intellij.openapi.editor.Editor 8 | import com.intellij.openapi.editor.EditorFactory 9 | import com.intellij.openapi.editor.event.BulkAwareDocumentListener 10 | import com.intellij.openapi.editor.event.DocumentEvent 11 | import com.smallcloud.refactai.modes.ModeProvider 12 | import com.smallcloud.refactai.io.InferenceGlobalContext.Companion.instance as InferenceGlobalContext 13 | 14 | 15 | class DocumentListener : BulkAwareDocumentListener, Disposable { 16 | override fun beforeDocumentChangeNonBulk(event: DocumentEvent) { 17 | Logger.getInstance("DocumentListener").debug("beforeDocumentChangeNonBulk") 18 | val editor = getActiveEditor(event.document) ?: return 19 | val provider = ModeProvider.getOrCreateModeProvider(editor) 20 | provider.beforeDocumentChangeNonBulk(event, editor) 21 | } 22 | 23 | override fun documentChangedNonBulk(event: DocumentEvent) { 24 | Logger.getInstance("DocumentListener").debug("documentChangedNonBulk") 25 | if (!InferenceGlobalContext.useAutoCompletion) return 26 | val editor = getActiveEditor(event.document) ?: return 27 | val provider = ModeProvider.getOrCreateModeProvider(editor) 28 | provider.onTextChange(event, editor, false) 29 | } 30 | 31 | private fun getActiveEditor(document: Document): Editor? { 32 | if (!ApplicationManager.getApplication().isDispatchThread) { 33 | return null 34 | } 35 | return EditorFactory.getInstance().getEditors(document).firstOrNull() 36 | } 37 | 38 | override fun dispose() {} 39 | } 40 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 🌟 Contribute to refact-intellij & refact-chat-js 2 | 3 | ## 📚 Table of Contents 4 | - [❤️ Ways to Contribute](#%EF%B8%8F-ways-to-contribute) 5 | - [🐛 Report Bugs](#-report-bugs) 6 | - [Instructions for React Chat build for JetBrains IDEs (to run locally)](#-instructions-for-react-chat-build-for-jetbrains-ides-to-run-locally) 7 | 8 | 9 | ### ❤️ Ways to Contribute 10 | 11 | * Fork the repository 12 | * Create a feature branch 13 | * Do the work 14 | * Create a pull request 15 | * Maintainers will review it 16 | 17 | 18 | ### 🐛 Report Bugs 19 | Encountered an issue? Help us improve Refact.ai by reporting bugs in issue section, make sure you label the issue with correct tag [here](https://github.com/smallcloudai/refact-intellij/issues)! 20 | 21 | 22 | 23 | ### 🔨 Instructions for React Chat build for JetBrains IDEs (to run locally) 24 | 1. Clone the branch alpha of the repository `refact-chat-js`. 25 | 2. Install dependencies and build the project: 26 | ```bash 27 | npm install && npm run build 28 | ``` 29 | 3. Clone the branch `dev` of the repository `refact-intellij`. 30 | 4. Move the generated `dist` directory from the `refact-chat-js` repository to the `src/main/resources/webview` directory of the `refact-intellij` repository. 31 | 5. Wait for the files to be indexed. 32 | 6. Open the IDE and navigate to the Gradle panel, then select Run Configurations with the suffix [runIde]. 33 | 7. In the Environment variables field, insert `REFACT_DEBUG=1`. 34 | 8. Start the project by right-clicking on the command `refact-intellij [runIde]`. 35 | 9. Inside the Refact.ai settings in the new IDE (PyCharm will open), select the field `Secret API Key` and press the key combination `Ctrl + Alt + - (minus)`, if using MacOS: `Command + Option + - (minus)`. 36 | 10. Scroll down and insert the port value for `xDebug LSP port`, which is the port under which LSP is running locally. By default, LSP's port is `8001`. 37 | 11. After that, you can test the chat functionality with latest features. 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Refact 3 |

4 | 5 | --- 6 | 7 | [![Discord](https://img.shields.io/discord/1037660742440194089?logo=discord&label=Discord&link=https%3A%2F%2Fsmallcloud.ai%2Fdiscord)](https://smallcloud.ai/discord) 8 | [![Twitter Follow](https://img.shields.io/twitter/follow/refact_ai)](https://twitter.com/intent/follow?screen_name=refact_ai) 9 | ![License](https://img.shields.io/github/license/smallcloudai/refact-intellij) 10 | 11 | 12 | # Refact-intellij 13 | *Refact for JetBrains is a free, open-source AI code assistant* 14 | 15 | ## Features 16 | 17 | - Access to 20+ LLMs: Leverage powerful language models, including GPT-4, Refact/1.6B, Code Llama, StarCoder2, Mistral, Mixtral, and more. Some models offer the ability to fine-tune for specialized needs. 18 | 19 | - CodeLens Integration: Get detailed insights into your code directly from your IDE using CodeLens. This feature enhances code navigation and understanding- CodeLens Integration: As you write code, you can instantly call a chat to find bugs or ask for an explanation of any code snippet. 20 | 21 | - FIM Debug Page: Access debugging tools and insights through the FIM debug page for improved troubleshooting. 22 | 23 | - Upload Images within Your IDE : Save time with our new in-IDE image upload feature—seamlessly integrated for your convenience. Easily upload one or multiple images to streamline your workflow 24 | 25 | 26 | ## Getting Started 27 | Once [installed](https://plugins.jetbrains.com/plugin/20647-refact-ai), look for the Refact.ai logo in the status bar or the sidebar, click 'login', and agree to T&C. Start typing some code, and autocomplete will make suggestions automatically! Press F1 to access the AI toolbox functions. Refact has a simple, user-friendly interface that makes it easy to use, even for those new to AI tools. 28 | 29 | If you have your own NVIDIA GPU, you can try the [self-hosted version](https://github.com/smallcloudai/refact). 30 | ## Support & Feedback 31 | Join our Discord to get to know other community members, send us feedback or suggestions, and get support. 32 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/PluginState.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai 2 | 3 | import com.intellij.openapi.Disposable 4 | import com.intellij.openapi.application.ApplicationManager 5 | import com.intellij.util.messages.MessageBus 6 | import com.intellij.util.messages.Topic 7 | import com.smallcloud.refactai.settings.AppSettingsState 8 | 9 | 10 | interface ExtraInfoChangedNotifier { 11 | fun tooltipMessageChanged(newMsg: String?) {} 12 | fun inferenceMessageChanged(newMsg: String?) {} 13 | fun loginMessageChanged(newMsg: String?) {} 14 | 15 | companion object { 16 | val TOPIC = Topic.create("Extra Info Changed Notifier", ExtraInfoChangedNotifier::class.java) 17 | } 18 | } 19 | 20 | class PluginState : Disposable { 21 | private val messageBus: MessageBus = ApplicationManager.getApplication().messageBus 22 | 23 | var tooltipMessage: String? = null 24 | get() = AppSettingsState.instance.tooltipMessage 25 | set(newMsg) { 26 | if (AppSettingsState.instance.tooltipMessage == newMsg) return 27 | messageBus 28 | .syncPublisher(ExtraInfoChangedNotifier.TOPIC) 29 | .tooltipMessageChanged(field) 30 | } 31 | 32 | var inferenceMessage: String? = null 33 | get() = AppSettingsState.instance.inferenceMessage 34 | set(newMsg) { 35 | if (field != newMsg) { 36 | field = newMsg 37 | messageBus 38 | .syncPublisher(ExtraInfoChangedNotifier.TOPIC) 39 | .inferenceMessageChanged(field) 40 | } 41 | } 42 | 43 | var loginMessage: String? 44 | get() = AppSettingsState.instance.loginMessage 45 | set(newMsg) { 46 | if (loginMessage == newMsg) return 47 | messageBus 48 | .syncPublisher(ExtraInfoChangedNotifier.TOPIC) 49 | .loginMessageChanged(newMsg) 50 | } 51 | 52 | override fun dispose() {} 53 | 54 | companion object { 55 | @JvmStatic 56 | val instance: PluginState 57 | get() = ApplicationManager.getApplication().getService(PluginState::class.java) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/codecompletion/RefactAIContinuousEvent.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.codecompletion 2 | 3 | import com.intellij.codeInsight.inline.completion.InlineCompletionEvent 4 | import com.intellij.codeInsight.inline.completion.InlineCompletionRequest 5 | import com.intellij.openapi.application.runReadAction 6 | import com.intellij.openapi.editor.Editor 7 | import com.intellij.openapi.project.Project 8 | import com.intellij.psi.PsiDocumentManager 9 | import com.intellij.psi.PsiFile 10 | import com.intellij.psi.impl.source.PsiFileImpl 11 | import com.intellij.psi.util.PsiUtilBase 12 | import com.intellij.util.concurrency.annotations.RequiresBlockingContext 13 | 14 | class RefactAIContinuousEvent(val editor: Editor, val offset: Int) : InlineCompletionEvent { 15 | override fun toRequest(): InlineCompletionRequest? { 16 | val project = editor.project ?: return null 17 | val file = getPsiFile(editor, project) ?: return null 18 | return InlineCompletionRequest(this, file, editor, editor.document, offset, offset) 19 | } 20 | } 21 | 22 | @RequiresBlockingContext 23 | private fun getPsiFile(editor: Editor, project: Project): PsiFile? { 24 | return runReadAction { 25 | try { 26 | val file = 27 | PsiDocumentManager.getInstance(project).getPsiFile(editor.document) ?: return@runReadAction null 28 | // * [PsiUtilBase] takes into account injected [PsiFile] (like in Jupyter Notebooks) 29 | // * However, it loads a file into the memory, which is expensive 30 | // * Some tests forbid loading a file when tearing down 31 | // * On tearing down, Lookup Cancellation happens, which causes the event 32 | // * Existence of [treeElement] guarantees that it's in the memory 33 | if (file.isLoadedInMemory()) { 34 | PsiUtilBase.getPsiFileInEditor(editor, project) 35 | } else { 36 | file 37 | } 38 | } catch (e: Exception) { 39 | return@runReadAction null 40 | } 41 | } 42 | } 43 | 44 | private fun PsiFile.isLoadedInMemory(): Boolean { 45 | return (this as? PsiFileImpl)?.treeElement != null 46 | } 47 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/settings/Host.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.settings 2 | 3 | import com.google.gson.JsonDeserializationContext 4 | import com.google.gson.JsonDeserializer 5 | import com.google.gson.JsonElement 6 | import java.lang.reflect.Type 7 | 8 | enum class HostKind(val value: String) { 9 | CLOUD("cloud"), 10 | SELF("self"), 11 | ENTERPRISE("enterprise"), 12 | BRING_YOUR_OWN_KEY("bring-your-own-key"), 13 | } 14 | 15 | sealed class Host { 16 | data class CloudHost(val apiKey: String, val sendCorrectedCodeSnippets: Boolean, val userName: String) : Host() 17 | 18 | data class SelfHost(val endpointAddress: String) : Host() 19 | 20 | data class Enterprise(val endpointAddress: String, val apiKey: String) : Host() 21 | 22 | data object BringYourOwnKey : Host() 23 | } 24 | 25 | class HostDeserializer : JsonDeserializer { 26 | override fun deserialize(p0: JsonElement?, p1: Type?, p2: JsonDeserializationContext?): Host? { 27 | val host = p0?.asJsonObject?.get("host")?.asJsonObject 28 | val type = host?.get("type")?.asString 29 | 30 | return when (type) { 31 | HostKind.CLOUD.value -> { 32 | val apiKey = host.get("apiKey")?.asString ?: return null 33 | val sendCorrectedCodeSnippets = host.get("sendCorrectedCodeSnippets")?.asBoolean?: false 34 | val userName = host.get("userName")?.asString?: ""; 35 | return Host.CloudHost(apiKey, sendCorrectedCodeSnippets, userName) 36 | } 37 | HostKind.SELF.value -> { 38 | val endpointAddress = host.get("endpointAddress")?.asString ?: return null 39 | return Host.SelfHost(endpointAddress) 40 | } 41 | HostKind.ENTERPRISE.value -> { 42 | val endpointAddress = host.get("endpointAddress")?.asString ?: return null 43 | val apiKey = host.get("apiKey")?.asString ?: return null 44 | return Host.Enterprise(endpointAddress, apiKey) 45 | } 46 | HostKind.BRING_YOUR_OWN_KEY.value -> { 47 | return Host.BringYourOwnKey 48 | } 49 | else -> null 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/modes/EventAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.modes 2 | 3 | import com.smallcloud.refactai.modes.completion.structs.DocumentEventExtra 4 | 5 | object EventAdapter { 6 | private fun bracketsAdapter( 7 | beforeText: List, 8 | afterText: List 9 | ): Pair?> { 10 | if (beforeText.size != 2 || afterText.size != 2) { 11 | return false to null 12 | } 13 | 14 | val startAutocompleteStrings = setOf("(", "\"", "{", "[", "'", "\"") 15 | val endAutocompleteStrings = setOf(")", "\"", "\'", "}", "]", "'''", "\"\"\"") 16 | val startToStopSymbols = mapOf( 17 | "(" to setOf(")"), "{" to setOf("}"), "[" to setOf("]"), 18 | "'" to setOf("'", "'''"), "\"" to setOf("\"", "\"\"\"") 19 | ) 20 | 21 | val firstEventFragment = afterText[beforeText.size - 2].event?.newFragment.toString() 22 | val secondEventFragment = afterText[beforeText.size - 1].event?.newFragment.toString() 23 | 24 | if (firstEventFragment.isEmpty() || firstEventFragment !in startAutocompleteStrings) { 25 | return false to null 26 | } 27 | if (secondEventFragment.isEmpty() || secondEventFragment !in endAutocompleteStrings) { 28 | return false to null 29 | } 30 | if (secondEventFragment !in startToStopSymbols.getValue(firstEventFragment)) { 31 | return false to null 32 | } 33 | 34 | return true to (beforeText.last() to afterText.last().copy( 35 | offsetCorrection = -1 36 | )) 37 | } 38 | 39 | fun eventProcess(beforeText: List, afterText: List) 40 | : Pair { 41 | if (beforeText.isNotEmpty() && afterText.isEmpty()) { 42 | return beforeText.last() to null 43 | } 44 | 45 | if (afterText.last().force) { 46 | return beforeText.last() to afterText.last() 47 | } 48 | 49 | val (succeed, events) = bracketsAdapter(beforeText, afterText) 50 | if (succeed && events != null) { 51 | return events 52 | } 53 | 54 | return beforeText.last() to afterText.last() 55 | } 56 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/panes/RefactAIToolboxPaneFactory.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.panes 2 | 3 | import com.intellij.ui.jcef.JBCefApp 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.util.Disposer 6 | import com.intellij.openapi.util.Key 7 | import com.intellij.openapi.wm.ToolWindow 8 | import com.intellij.openapi.wm.ToolWindowFactory 9 | import com.intellij.openapi.wm.ToolWindowManager 10 | import com.intellij.ui.content.Content 11 | import com.intellij.ui.content.ContentFactory 12 | import com.smallcloud.refactai.Resources 13 | import com.smallcloud.refactai.panes.sharedchat.ChatPanes 14 | import com.smallcloud.refactai.utils.getLastUsedProject 15 | 16 | 17 | class RefactAIToolboxPaneFactory : ToolWindowFactory { 18 | override fun init(toolWindow: ToolWindow) { 19 | toolWindow.setIcon(Resources.Icons.LOGO_RED_13x13) 20 | super.init(toolWindow) 21 | } 22 | 23 | override suspend fun isApplicableAsync(project: Project): Boolean = JBCefApp.isSupported() 24 | 25 | override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { 26 | val contentFactory = ContentFactory.getInstance() 27 | val chatPanes = ChatPanes(project) 28 | Disposer.register(toolWindow.disposable, chatPanes) 29 | val content: Content = contentFactory.createContent(chatPanes.getComponent(), null, true) 30 | content.isCloseable = false 31 | content.putUserData(panesKey, chatPanes) 32 | toolWindow.contentManager.addContent(content) 33 | } 34 | 35 | companion object { 36 | private val panesKey = Key.create("refact.panes") 37 | val chat: ChatPanes? 38 | get() { 39 | val tw = ToolWindowManager.getInstance(getLastUsedProject()).getToolWindow("Refact") 40 | return tw?.contentManager?.getContent(0)?.getUserData(panesKey) 41 | } 42 | 43 | fun focusChat() { 44 | val tw = ToolWindowManager.getInstance(getLastUsedProject()).getToolWindow("Refact") 45 | val content = tw?.contentManager?.getContent(0) ?: return 46 | tw.contentManager.setSelectedContent(content, true) 47 | val panes = content.getUserData(panesKey) 48 | panes?.requestFocus() 49 | chat?.newChat() 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/modes/EditorTextState.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.modes 2 | 3 | import com.intellij.openapi.editor.Document 4 | import com.intellij.openapi.editor.Editor 5 | 6 | class EditorTextState( 7 | val editor: Editor, 8 | val modificationStamp: Long, 9 | var offset: Int 10 | ) { 11 | var text: String 12 | val document: Document 13 | val lines: List 14 | val currentLineNumber: Int 15 | val currentLine: String 16 | val currentLineStartOffset: Int 17 | val currentLineEndOffset: Int 18 | val offsetByCurrentLine: Int 19 | private val initialOffset: Int 20 | 21 | init { 22 | text = editor.document.text 23 | document = editor.document 24 | lines = document.text.split("\n", "\r\n") 25 | currentLineNumber = document.getLineNumber(offset) 26 | currentLine = lines[currentLineNumber] 27 | currentLineStartOffset = document.getLineStartOffset(currentLineNumber) 28 | currentLineEndOffset = document.getLineEndOffset(currentLineNumber) 29 | offsetByCurrentLine = offset - currentLineStartOffset 30 | initialOffset = offset 31 | } 32 | 33 | fun currentLineIsEmptySymbols(): Boolean { 34 | if (currentLine.isEmpty()) return false 35 | return currentLine.substring(offsetByCurrentLine).isEmpty() && 36 | currentLine.substring(0, offsetByCurrentLine) 37 | .replace("\t", "") 38 | .replace(" ", "").isEmpty() 39 | } 40 | 41 | fun getRidOfLeftSpacesInplace() { 42 | if (!currentLineIsEmptySymbols()) return 43 | 44 | val before = if (currentLineNumber == 0) 45 | "" else lines.subList(0, currentLineNumber).joinToString("\n", postfix = "\n") 46 | val after = if (currentLineNumber == lines.size - 1) 47 | "" else lines.subList(currentLineNumber + 1, lines.size).joinToString("\n", prefix = "\n") 48 | text = before + after 49 | offset = before.length 50 | } 51 | 52 | fun restoreInplace() { 53 | if (!currentLineIsEmptySymbols()) return 54 | if (offset == initialOffset) return 55 | 56 | text = editor.document.text 57 | offset = initialOffset 58 | } 59 | 60 | fun isValid(): Boolean { 61 | return lines.size == document.lineCount 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/utils/JSQueryManager.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.utils 2 | 3 | import com.intellij.openapi.Disposable 4 | import com.intellij.openapi.diagnostic.Logger 5 | import com.intellij.ui.jcef.JBCefBrowser 6 | import com.intellij.ui.jcef.JBCefBrowserBase 7 | import com.intellij.ui.jcef.JBCefJSQuery 8 | 9 | /** 10 | * Manages JavaScript query objects to ensure proper disposal and prevent memory leaks. 11 | * This class tracks all created JS queries and provides centralized disposal. 12 | */ 13 | class JSQueryManager(private val browser: JBCefBrowser) : Disposable { 14 | private val logger = Logger.getInstance(JSQueryManager::class.java) 15 | private val queries = mutableListOf() 16 | private var disposed = false 17 | 18 | fun createQuery(handler: (String) -> JBCefJSQuery.Response?): JBCefJSQuery { 19 | if (disposed) { 20 | throw IllegalStateException("JSQueryManager has been disposed") 21 | } 22 | 23 | try { 24 | val query = JBCefJSQuery.create(browser as JBCefBrowserBase) 25 | query.addHandler(handler) 26 | synchronized(queries) { 27 | queries.add(query) 28 | } 29 | logger.info("Created JS query. Total queries: ${queries.size}") 30 | return query 31 | } catch (e: Exception) { 32 | logger.error("Failed to create JS query", e) 33 | throw e 34 | } 35 | } 36 | 37 | fun createStringQuery(handler: (String) -> Unit): JBCefJSQuery { 38 | return createQuery { msg -> 39 | try { 40 | handler(msg) 41 | null // No response needed 42 | } catch (e: Exception) { 43 | logger.warn("Error in JS query handler", e) 44 | null 45 | } 46 | } 47 | } 48 | 49 | override fun dispose() { 50 | if (disposed) { 51 | return 52 | } 53 | disposed = true 54 | synchronized(queries) { 55 | logger.info("Disposing JSQueryManager with ${queries.size} queries") 56 | queries.forEach { query -> 57 | try { 58 | query.dispose() 59 | } catch (e: Exception) { 60 | logger.warn("Error disposing JS query during cleanup", e) 61 | } 62 | } 63 | queries.clear() 64 | logger.info("JSQueryManager disposal completed") 65 | } 66 | } 67 | 68 | fun isDisposed(): Boolean = disposed 69 | } 70 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/status_bar/StatusBarComponent.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.status_bar 2 | 3 | import com.intellij.ide.ui.UISettings.Companion.setupAntialiasing 4 | import com.intellij.openapi.util.IconLoader 5 | import com.intellij.openapi.wm.impl.status.TextPanel 6 | import com.intellij.ui.scale.JBUIScale 7 | import com.intellij.util.ui.UIUtil 8 | import java.awt.Color 9 | import java.awt.Dimension 10 | import java.awt.Graphics 11 | import java.awt.Graphics2D 12 | import javax.swing.Icon 13 | 14 | class StatusBarComponent: TextPanel { 15 | companion object { 16 | private val GAP = JBUIScale.scale(3) 17 | } 18 | 19 | var icon: Icon? = null 20 | var bottomLineColor: Color = UIUtil.getPanelBackground() 21 | 22 | constructor() : super(null) 23 | constructor(toolTipTextSupplier: (() -> String?)?) : super(toolTipTextSupplier) 24 | 25 | override fun paintComponent(g: Graphics) { 26 | val panelWidth = width 27 | val panelHeight = height 28 | g as Graphics2D 29 | setupAntialiasing(g) 30 | g.setColor(bottomLineColor) 31 | g.fillRect(0, panelHeight - GAP, panelWidth, GAP) 32 | super.paintComponent(g) 33 | val icon = if (icon == null || isEnabled) icon else IconLoader.getDisabledIcon(icon!!) 34 | icon?.paintIcon(this, g, getIconX(g), height / 2 - icon.iconHeight / 2 - 1) 35 | } 36 | 37 | override fun getPreferredSize(): Dimension { 38 | val preferredSize = super.getPreferredSize() 39 | return if (icon == null) { 40 | preferredSize 41 | } else { 42 | Dimension((preferredSize.width + icon!!.iconWidth + GAP).coerceAtLeast(height), preferredSize.height) 43 | } 44 | } 45 | 46 | override fun getTextX(g: Graphics): Int { 47 | val x = super.getTextX(g) 48 | return when { 49 | icon == null || alignment == RIGHT_ALIGNMENT -> x 50 | alignment == CENTER_ALIGNMENT -> x + (icon!!.iconWidth + GAP) / 2 51 | alignment == LEFT_ALIGNMENT -> x + icon!!.iconWidth + GAP 52 | else -> x 53 | } 54 | } 55 | 56 | private fun getIconX(g: Graphics): Int { 57 | val x = super.getTextX(g) 58 | return when { 59 | icon == null || text == null || alignment == LEFT_ALIGNMENT -> x 60 | alignment == CENTER_ALIGNMENT -> x - (icon!!.iconWidth + GAP) / 2 61 | alignment == RIGHT_ALIGNMENT -> x - icon!!.iconWidth - GAP 62 | else -> x 63 | } 64 | } 65 | 66 | 67 | 68 | fun hasIcon(): Boolean = icon != null 69 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/listeners/LastEditorGetterListener.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.listeners 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.editor.Editor 5 | import com.intellij.openapi.editor.EditorFactory 6 | import com.intellij.openapi.editor.event.EditorFactoryEvent 7 | import com.intellij.openapi.editor.event.EditorFactoryListener 8 | import com.intellij.openapi.editor.ex.EditorEx 9 | import com.intellij.openapi.editor.ex.FocusChangeListener 10 | import com.intellij.openapi.fileEditor.FileDocumentManager 11 | import com.intellij.openapi.fileEditor.FileEditorManager 12 | import com.intellij.openapi.fileEditor.FileEditorManagerListener 13 | import com.intellij.openapi.vfs.VirtualFile 14 | import com.intellij.util.messages.Topic 15 | import com.smallcloud.refactai.PluginState 16 | 17 | 18 | interface SelectionChangedNotifier { 19 | fun isEditorChanged(editor: Editor?) {} 20 | 21 | companion object { 22 | val TOPIC = Topic.create("Selection Changed Notifier", SelectionChangedNotifier::class.java) 23 | } 24 | } 25 | 26 | class LastEditorGetterListener : EditorFactoryListener, FileEditorManagerListener { 27 | private val focusChangeListener = object : FocusChangeListener { 28 | override fun focusGained(editor: Editor) { 29 | setEditor(editor) 30 | } 31 | } 32 | 33 | init { 34 | ApplicationManager.getApplication() 35 | .messageBus.connect(PluginState.instance) 36 | .subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, this) 37 | instance = this 38 | } 39 | 40 | private fun setEditor(editor: Editor) { 41 | if (LAST_EDITOR != editor) { 42 | LAST_EDITOR = editor 43 | ApplicationManager.getApplication().messageBus 44 | .syncPublisher(SelectionChangedNotifier.TOPIC) 45 | .isEditorChanged(editor) 46 | } 47 | } 48 | 49 | private fun setup(editor: Editor) { 50 | (editor as EditorEx).addFocusListener(focusChangeListener) 51 | } 52 | 53 | private fun getVirtualFile(editor: Editor): VirtualFile? { 54 | return FileDocumentManager.getInstance().getFile(editor.document) 55 | } 56 | 57 | override fun fileOpened(source: FileEditorManager, file: VirtualFile) { 58 | val editor = EditorFactory.getInstance().allEditors.firstOrNull { getVirtualFile(it) == file } 59 | if (editor != null) { 60 | setup(editor) 61 | } 62 | } 63 | 64 | override fun editorCreated(event: EditorFactoryEvent) {} 65 | 66 | companion object { 67 | lateinit var instance: LastEditorGetterListener 68 | var LAST_EDITOR: Editor? = null 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/modes/diff/waitingDiff.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.modes.diff 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.editor.Editor 5 | import com.intellij.openapi.editor.LogicalPosition 6 | import com.intellij.openapi.editor.markup.HighlighterTargetArea 7 | import com.intellij.openapi.editor.markup.RangeHighlighter 8 | import com.intellij.openapi.editor.markup.TextAttributes 9 | import java.awt.Color 10 | import kotlin.math.floor 11 | import kotlin.math.min 12 | import kotlin.math.sin 13 | 14 | 15 | fun waitingDiff( 16 | editor: Editor, 17 | startPosition: LogicalPosition, finishPosition: LogicalPosition, 18 | isProgress: () -> Boolean 19 | ) { 20 | val colors: MutableList = emptyList().toMutableList() 21 | for (c in 0 until 20) { 22 | val phase: Float = c.toFloat() / 10 23 | colors.add( 24 | Color( 25 | maxOf(100, floor(255 * sin(phase * Math.PI + Math.PI)).toInt()), 26 | maxOf(100, floor(255 * sin(phase * Math.PI + Math.PI / 2)).toInt()), 27 | maxOf(100, floor(255 * sin(phase * Math.PI + 3 * Math.PI / 2)).toInt()), 28 | (255 * 0.3).toInt() 29 | ) 30 | ) 31 | } 32 | 33 | var t = 0 34 | val rangeHighlighters: MutableList = mutableListOf() 35 | try { 36 | while (isProgress()) { 37 | Thread.sleep(100) 38 | ApplicationManager.getApplication().invokeAndWait { 39 | rangeHighlighters.forEach { editor.markupModel.removeHighlighter(it) } 40 | rangeHighlighters.clear() 41 | for (lineNumber in startPosition.line..finishPosition.line) { 42 | val localStartOffset = editor.document.getLineStartOffset(lineNumber) 43 | val localEndOffset = editor.document.getLineEndOffset(lineNumber) 44 | for (c in localStartOffset until localEndOffset step 2) { 45 | val a = (lineNumber + c + t) % colors.size 46 | rangeHighlighters.add( 47 | editor.markupModel.addRangeHighlighter( 48 | c, min(c + 2, localEndOffset), 9999, 49 | TextAttributes().apply { 50 | backgroundColor = colors[a] 51 | }, 52 | HighlighterTargetArea.EXACT_RANGE 53 | ) 54 | ) 55 | 56 | } 57 | } 58 | } 59 | 60 | t++ 61 | } 62 | } finally { 63 | ApplicationManager.getApplication().invokeAndWait { 64 | rangeHighlighters.forEach { editor.markupModel.removeHighlighter(it) } 65 | rangeHighlighters.clear() 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/smallcloud/refactai/lsp/LSPProcessHolderTimeoutTest.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.lsp 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.smallcloud.refactai.testUtils.MockServer 5 | import okhttp3.mockwebserver.MockResponse 6 | import org.junit.Test 7 | import org.junit.Ignore 8 | import java.net.URI 9 | import java.util.concurrent.TimeUnit 10 | 11 | /** 12 | * This test demonstrates the HTTP timeout issue in LSP request handling 13 | * by directly testing HTTP requests with mocked components. 14 | */ 15 | class LSPProcessHolderTimeoutTest : MockServer() { 16 | 17 | class TestLSPProcessHolder(project: Project, baseUrl: String) : LSPProcessHolder(project) { 18 | override val url = URI(baseUrl) 19 | 20 | override var isWorking: Boolean 21 | get() = true 22 | set(value) { /* Do nothing */ } 23 | 24 | override fun startProcess() { 25 | // Do nothing to avoid actual process starting 26 | } 27 | } 28 | 29 | /** 30 | * Test the HTTP request/response handling similar to LSPProcessHolder.fetchCustomization() 31 | */ 32 | @Test 33 | fun fetchCustomization() { 34 | // Create a successful response with a delay 35 | val response = MockResponse() 36 | .setResponseCode(200) 37 | .setHeader("Content-Type", "application/json") 38 | .setBody("{\"result\": \"delayed response\"}") 39 | .setBodyDelay(100, TimeUnit.MILLISECONDS) // Add a small delay 40 | 41 | // Queue the response 42 | this.server.enqueue(response) 43 | 44 | val lspProcessHolder = TestLSPProcessHolder(this.project, baseUrl) 45 | val result = lspProcessHolder.fetchCustomization() 46 | val recordedRequest = this.server.takeRequest(5, TimeUnit.SECONDS) 47 | 48 | assertNotNull("Request should have been recorded", recordedRequest) 49 | assertNotNull("Result should not be null", result) 50 | assertEquals("{\"result\":\"delayed response\"}", result.toString()) 51 | } 52 | 53 | @Ignore("very slow") 54 | @Test 55 | fun fetchCustomizationWithTimeout() { 56 | // Create a successful response with a delay 57 | val response = MockResponse() 58 | .setResponseCode(200) 59 | .setHeader("Content-Type", "application/json") 60 | .setBody("{\"result\": \"delayed response\"}") 61 | .setHeadersDelay(60, TimeUnit.SECONDS) 62 | 63 | // Queue the response 64 | this.server.enqueue(response) 65 | 66 | val lspProcessHolder = TestLSPProcessHolder(this.project, baseUrl) 67 | val result = lspProcessHolder.fetchCustomization() 68 | val recordedRequest = this.server.takeRequest() 69 | 70 | assertNotNull("Request should have been recorded", recordedRequest) 71 | assertNull("Result should not be null", result) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/code_lens/RefactCodeVisionProviderFactory.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.code_lens 2 | 3 | import com.intellij.codeInsight.codeVision.CodeVisionProvider 4 | import com.intellij.codeInsight.codeVision.CodeVisionProviderFactory 5 | import com.intellij.codeInsight.codeVision.settings.CodeVisionGroupSettingProvider 6 | import com.intellij.openapi.application.ApplicationManager 7 | import com.intellij.openapi.components.service 8 | import com.intellij.openapi.project.Project 9 | import com.smallcloud.refactai.RefactAIBundle 10 | import com.smallcloud.refactai.lsp.LSPProcessHolder.Companion.initialize 11 | import com.smallcloud.refactai.lsp.LSPProcessHolder.Companion.getInstance as getLSPProcessHolder 12 | 13 | // hardcode default codelens from lsp customization 14 | class RefactOpenChatSettingProvider : CodeVisionGroupSettingProvider { 15 | override val groupId: String 16 | get() = makeIdForProvider("open_chat") 17 | override val groupName: String 18 | get() = RefactAIBundle.message("codeVision.openChat.name") 19 | } 20 | 21 | class RefactOpenProblemsSettingProvider : CodeVisionGroupSettingProvider { 22 | override val groupId: String 23 | get() = makeIdForProvider("problems") 24 | override val groupName: String 25 | get() = RefactAIBundle.message("codeVision.problems.name") 26 | } 27 | 28 | class RefactOpenExplainSettingProvider : CodeVisionGroupSettingProvider { 29 | override val groupId: String 30 | get() = makeIdForProvider("explain") 31 | override val groupName: String 32 | get() = RefactAIBundle.message("codeVision.explain.name") 33 | } 34 | 35 | class RefactCodeVisionProviderFactory : CodeVisionProviderFactory { 36 | override fun createProviders(project: Project): Sequence> { 37 | if (ApplicationManager.getApplication().isUnitTestMode) return emptySequence() 38 | initialize() 39 | val customization = getLSPProcessHolder(project)?.fetchCustomization() ?: return emptySequence() 40 | if (customization.has("code_lens")) { 41 | val allCodeLenses = customization.get("code_lens").asJsonObject 42 | val allCodeLensKeys = allCodeLenses.keySet().toList() 43 | val providers: MutableList> = mutableListOf() 44 | for ((idx, key) in allCodeLensKeys.withIndex()) { 45 | val label = allCodeLenses.get(key).asJsonObject.get("label").asString 46 | var posAfter: String? = null 47 | if (idx != 0) { 48 | posAfter = allCodeLensKeys[idx - 1] 49 | } 50 | providers.add(RefactCodeVisionProvider(key, posAfter, label, customization)) 51 | } 52 | val ids = providers.map { it.id } 53 | project.service().setCodeLensIds(ids) 54 | 55 | return providers.asSequence() 56 | 57 | } 58 | return emptySequence() 59 | } 60 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/io/CloudMessageService.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.io 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.JsonObject 5 | import com.intellij.openapi.Disposable 6 | import com.intellij.openapi.application.ApplicationManager 7 | import com.intellij.openapi.components.Service 8 | import com.smallcloud.refactai.Resources.cloudUserMessage 9 | import com.smallcloud.refactai.account.AccountManagerChangedNotifier 10 | import com.smallcloud.refactai.PluginState.Companion.instance as PluginState 11 | import com.smallcloud.refactai.account.AccountManager.Companion.instance as AccountManager 12 | import com.smallcloud.refactai.io.InferenceGlobalContext.Companion.instance as InferenceGlobalContext 13 | 14 | @Service 15 | class CloudMessageService : Disposable { 16 | init { 17 | if (InferenceGlobalContext.isCloud && !AccountManager.apiKey.isNullOrEmpty()) { 18 | updateLoginMessage() 19 | } 20 | ApplicationManager.getApplication().messageBus.connect(this) 21 | .subscribe(InferenceGlobalContextChangedNotifier.TOPIC, object : InferenceGlobalContextChangedNotifier { 22 | override fun userInferenceUriChanged(newUrl: String?) { 23 | if (InferenceGlobalContext.isCloud && !AccountManager.apiKey.isNullOrEmpty()) { 24 | updateLoginMessage() 25 | } 26 | } 27 | }) 28 | ApplicationManager.getApplication().messageBus.connect(this) 29 | .subscribe(AccountManagerChangedNotifier.TOPIC, object : AccountManagerChangedNotifier { 30 | override fun apiKeyChanged(newApiKey: String?) { 31 | if (InferenceGlobalContext.isCloud && !AccountManager.apiKey.isNullOrEmpty()) { 32 | updateLoginMessage() 33 | } 34 | } 35 | }) 36 | 37 | } 38 | 39 | private fun updateLoginMessage() { 40 | AccountManager.apiKey?.let { apiKey -> 41 | InferenceGlobalContext.connection.get(cloudUserMessage, 42 | headers = mapOf("Authorization" to "Bearer $apiKey"), 43 | dataReceiveEnded = { 44 | Gson().fromJson(it, JsonObject::class.java).let { value -> 45 | if (value.has("retcode") && value.get("retcode").asString != null) { 46 | val retcode = value.get("retcode").asString 47 | if (retcode == "OK") { 48 | if (value.has("message") && value.get("message").asString != null) { 49 | PluginState.loginMessage = value.get("message").asString 50 | } 51 | } 52 | } 53 | } 54 | }, failedDataReceiveEnded = { 55 | InferenceGlobalContext.status = ConnectionStatus.ERROR 56 | if (it != null) { 57 | InferenceGlobalContext.lastErrorMsg = it.message 58 | } 59 | }) 60 | } 61 | 62 | } 63 | 64 | override fun dispose() {} 65 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/account/AccountManager.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.account 2 | 3 | import com.intellij.openapi.Disposable 4 | import com.intellij.openapi.application.ApplicationManager 5 | import com.smallcloud.refactai.io.InferenceGlobalContext 6 | import com.smallcloud.refactai.settings.AppSettingsState 7 | 8 | class AccountManager: Disposable { 9 | private var previousLoggedInState: Boolean = false 10 | 11 | var user: String? 12 | get() = AppSettingsState.instance.userLoggedIn 13 | set(newUser) { 14 | if (newUser == user) return 15 | ApplicationManager.getApplication() 16 | .messageBus 17 | .syncPublisher(AccountManagerChangedNotifier.TOPIC) 18 | .userChanged(newUser) 19 | checkLoggedInAndNotifyIfNeed() 20 | } 21 | var apiKey: String? 22 | get() = AppSettingsState.instance.apiKey 23 | set(newApiKey) { 24 | if (newApiKey == apiKey) return 25 | ApplicationManager.getApplication() 26 | .messageBus 27 | .syncPublisher(AccountManagerChangedNotifier.TOPIC) 28 | .apiKeyChanged(newApiKey) 29 | checkLoggedInAndNotifyIfNeed() 30 | } 31 | var activePlan: String? = null 32 | set(newPlan) { 33 | if (newPlan == field) return 34 | field = newPlan 35 | ApplicationManager.getApplication() 36 | .messageBus 37 | .syncPublisher(AccountManagerChangedNotifier.TOPIC) 38 | .planStatusChanged(newPlan) 39 | } 40 | 41 | val isLoggedIn: Boolean 42 | get() { 43 | return !apiKey.isNullOrEmpty() 44 | } 45 | 46 | var meteringBalance: Int? = null 47 | set(newValue) { 48 | if (newValue == field) return 49 | field = newValue 50 | ApplicationManager.getApplication() 51 | .messageBus 52 | .syncPublisher(AccountManagerChangedNotifier.TOPIC) 53 | .meteringBalanceChanged(newValue) 54 | } 55 | 56 | private fun loadFromSettings() { 57 | previousLoggedInState = isLoggedIn 58 | } 59 | 60 | fun startup() { 61 | loadFromSettings() 62 | } 63 | 64 | private fun checkLoggedInAndNotifyIfNeed() { 65 | if (previousLoggedInState == isLoggedIn) return 66 | previousLoggedInState = isLoggedIn 67 | loginChangedNotify(isLoggedIn) 68 | } 69 | 70 | private fun loginChangedNotify(isLoggedIn: Boolean) { 71 | ApplicationManager.getApplication() 72 | .messageBus 73 | .syncPublisher(AccountManagerChangedNotifier.TOPIC) 74 | .isLoggedInChanged(isLoggedIn) 75 | } 76 | 77 | fun logout() { 78 | apiKey = null 79 | InferenceGlobalContext.instance.inferenceUri = null 80 | user = null 81 | meteringBalance = null 82 | } 83 | 84 | override fun dispose() {} 85 | 86 | companion object { 87 | @JvmStatic 88 | val instance: AccountManager 89 | get() = ApplicationManager.getApplication().getService(AccountManager::class.java) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/modes/diff/renderer/BlockRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.modes.diff.renderer 2 | 3 | 4 | import com.intellij.openapi.editor.Editor 5 | import com.intellij.openapi.editor.EditorCustomElementRenderer 6 | import com.intellij.openapi.editor.Inlay 7 | import com.intellij.openapi.editor.markup.TextAttributes 8 | import dev.gitlive.difflib.patch.Patch 9 | import java.awt.Color 10 | import java.awt.Graphics 11 | import java.awt.Rectangle 12 | 13 | 14 | open class BlockElementRenderer( 15 | private val color: Color, 16 | private val veryColor: Color, 17 | private val editor: Editor, 18 | private val blockText: List, 19 | private val smallPatches: List>, 20 | private val deprecated: Boolean 21 | ) : EditorCustomElementRenderer { 22 | 23 | override fun calcWidthInPixels(inlay: Inlay<*>): Int { 24 | val line = blockText.maxByOrNull { it.length } 25 | return editor.contentComponent 26 | .getFontMetrics(RenderHelper.getFont(editor, deprecated)).stringWidth(line!!) 27 | } 28 | 29 | override fun calcHeightInPixels(inlay: Inlay<*>): Int { 30 | return editor.lineHeight * blockText.size 31 | } 32 | 33 | override fun paint( 34 | inlay: Inlay<*>, 35 | g: Graphics, 36 | targetRegion: Rectangle, 37 | textAttributes: TextAttributes 38 | ) { 39 | val highlightG = g.create() 40 | highlightG.color = color 41 | highlightG.fillRect(targetRegion.x, targetRegion.y, 9999999, targetRegion.height) 42 | g.font = RenderHelper.getFont(editor, deprecated) 43 | g.color = editor.colorsScheme.defaultForeground 44 | val metric = g.getFontMetrics(g.font) 45 | 46 | val smallPatchesG = g.create() 47 | smallPatchesG.color = veryColor 48 | smallPatches.withIndex().forEach { (i, patch) -> 49 | val currentLine = blockText[i] 50 | patch.getDeltas().forEach { 51 | val startBound = g.font.getStringBounds( 52 | currentLine.substring(0, it.target.position), 53 | metric.fontRenderContext 54 | ) 55 | val endBound = g.font.getStringBounds( 56 | currentLine.substring(0, it.target.position + it.target.size()), 57 | metric.fontRenderContext 58 | ) 59 | smallPatchesG.fillRect( 60 | targetRegion.x + startBound.width.toInt(), 61 | targetRegion.y + i * editor.lineHeight, 62 | (endBound.width - startBound.width).toInt(), 63 | editor.lineHeight 64 | ) 65 | } 66 | } 67 | blockText.withIndex().forEach { (i, line) -> 68 | g.drawString( 69 | line, 70 | 0, 71 | targetRegion.y + i * editor.lineHeight + editor.ascent 72 | ) 73 | } 74 | } 75 | } 76 | 77 | class InsertBlockElementRenderer( 78 | private val editor: Editor, 79 | private val blockText: List, 80 | private val smallPatches: List>, 81 | private val deprecated: Boolean 82 | ) : BlockElementRenderer(greenColor, veryGreenColor, editor, blockText, smallPatches, deprecated) 83 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/Initializer.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai 2 | 3 | import com.intellij.openapi.application.ApplicationInfo 4 | import com.intellij.openapi.util.registry.Registry 5 | import com.intellij.ui.jcef.JBCefApp 6 | import com.intellij.ide.plugins.PluginInstaller 7 | import com.intellij.openapi.Disposable 8 | import com.intellij.openapi.application.ApplicationManager 9 | import com.intellij.openapi.application.invokeLater 10 | import com.intellij.openapi.diagnostic.Logger 11 | import com.intellij.openapi.project.Project 12 | import com.intellij.openapi.startup.ProjectActivity 13 | import com.smallcloud.refactai.io.CloudMessageService 14 | import com.smallcloud.refactai.listeners.UninstallListener 15 | import com.smallcloud.refactai.lsp.LSPActiveDocNotifierService 16 | import com.smallcloud.refactai.lsp.LSPProcessHolder.Companion.initialize 17 | import com.smallcloud.refactai.notifications.emitInfo 18 | import com.smallcloud.refactai.notifications.notificationStartup 19 | import com.smallcloud.refactai.panes.sharedchat.ChatPaneInvokeAction 20 | import com.smallcloud.refactai.settings.AppSettingsState 21 | import com.smallcloud.refactai.settings.settingsStartup 22 | import java.util.concurrent.atomic.AtomicBoolean 23 | import com.smallcloud.refactai.lsp.LSPProcessHolder.Companion.getInstance as getLSPProcessHolder 24 | 25 | class Initializer : ProjectActivity, Disposable { 26 | override suspend fun execute(project: Project) { 27 | val shouldInitialize = !(initialized.getAndSet(true) || ApplicationManager.getApplication().isUnitTestMode) 28 | if (shouldInitialize) { 29 | Logger.getInstance("SMCInitializer").info("Bin prefix = ${Resources.binPrefix}") 30 | initialize() 31 | if (AppSettingsState.instance.isFirstStart) { 32 | AppSettingsState.instance.isFirstStart = false 33 | invokeLater { ChatPaneInvokeAction().actionPerformed() } 34 | } 35 | settingsStartup() 36 | notificationStartup() 37 | PluginInstaller.addStateListener(UninstallListener()) 38 | UpdateChecker.instance 39 | 40 | ApplicationManager.getApplication().getService(CloudMessageService::class.java) 41 | if (!JBCefApp.isSupported()) { 42 | emitInfo(RefactAIBundle.message("notifications.chatCanNotStartWarning"), false) 43 | } 44 | 45 | // notifications.chatCanFreezeWarning 46 | // Show warning for 2025.* IDE versions with JCEF out-of-process enabled 47 | if (JBCefApp.isSupported()) { 48 | val appInfo = ApplicationInfo.getInstance() 49 | val is2025 = appInfo.majorVersion == "2025" 50 | val outOfProc = try { 51 | Registry.get("ide.browser.jcef.out-of-process.enabled").asBoolean() 52 | } catch (_: Throwable) { 53 | false 54 | } 55 | if (is2025 && outOfProc) { 56 | emitInfo(RefactAIBundle.message("notifications.chatCanFreezeWarning"), false) 57 | } 58 | } 59 | } 60 | getLSPProcessHolder(project) 61 | project.getService(LSPActiveDocNotifierService::class.java) 62 | } 63 | 64 | override fun dispose() { 65 | } 66 | 67 | } 68 | 69 | private val initialized = AtomicBoolean(false) -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/lsp/LSPConfig.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.lsp 2 | 3 | import com.smallcloud.refactai.struct.DeploymentMode 4 | 5 | data class LSPConfig( 6 | val address: String? = null, 7 | var port: Int? = null, 8 | var apiKey: String? = null, 9 | var clientVersion: String? = null, 10 | var useTelemetry: Boolean = false, 11 | var deployment: DeploymentMode = DeploymentMode.CLOUD, 12 | var ast: Boolean = true, 13 | var astFileLimit: Int? = null, 14 | var vecdb: Boolean = true, 15 | var vecdbFileLimit: Int? = null, 16 | var insecureSSL: Boolean = false, 17 | val experimental: Boolean = false 18 | ) { 19 | fun toArgs(): List { 20 | val params = mutableListOf() 21 | if (address != null) { 22 | params.add("--address-url") 23 | params.add("$address") 24 | } 25 | if (port != null) { 26 | params.add("--http-port") 27 | params.add("$port") 28 | } 29 | if (apiKey != null) { 30 | params.add("--api-key") 31 | params.add("$apiKey") 32 | } 33 | if (clientVersion != null) { 34 | params.add("--enduser-client-version") 35 | params.add("$clientVersion") 36 | } 37 | if (useTelemetry) { 38 | params.add("--basic-telemetry") 39 | } 40 | if (ast) { 41 | params.add("--ast") 42 | } 43 | if (ast && astFileLimit != null) { 44 | params.add("--ast-max-files") 45 | params.add("$astFileLimit") 46 | } 47 | if (vecdb) { 48 | params.add("--vecdb") 49 | } 50 | if (vecdb && vecdbFileLimit != null) { 51 | params.add("--vecdb-max-files") 52 | params.add("$vecdbFileLimit") 53 | } 54 | if (insecureSSL) { 55 | params.add("--insecure") 56 | } 57 | if (experimental) { 58 | params.add("--experimental") 59 | } 60 | return params 61 | } 62 | 63 | override fun equals(other: Any?): Boolean { 64 | if (this === other) return true 65 | if (javaClass != other?.javaClass) return false 66 | 67 | other as LSPConfig 68 | 69 | if (address != other.address) return false 70 | if (apiKey != other.apiKey) return false 71 | if (clientVersion != other.clientVersion) return false 72 | if (useTelemetry != other.useTelemetry) return false 73 | if (deployment != other.deployment) return false 74 | if (ast != other.ast) return false 75 | if (vecdb != other.vecdb) return false 76 | if (astFileLimit != other.astFileLimit) return false 77 | if (vecdbFileLimit != other.vecdbFileLimit) return false 78 | if (experimental != other.experimental) return false 79 | 80 | return true 81 | } 82 | 83 | val isValid: Boolean 84 | get() { 85 | return address != null 86 | && port != null 87 | && clientVersion != null 88 | && (astFileLimit != null && astFileLimit!! > 0) 89 | && (vecdbFileLimit != null && vecdbFileLimit!! > 0) 90 | // token must be if we are not selfhosted 91 | && (deployment == DeploymentMode.SELF_HOSTED || 92 | (apiKey != null && (deployment == DeploymentMode.CLOUD || deployment == DeploymentMode.HF))) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/utils/CefLifecycleManager.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.utils 2 | 3 | import com.intellij.openapi.diagnostic.Logger 4 | import org.cef.CefApp 5 | import org.cef.CefClient 6 | import org.cef.browser.CefBrowser 7 | 8 | /** 9 | * Centralized CEF lifecycle management to prevent resource leaks and ensure proper cleanup. 10 | * This singleton manages all CEF browser instances and handles proper initialization/disposal. 11 | */ 12 | object CefLifecycleManager { 13 | private val logger = Logger.getInstance(CefLifecycleManager::class.java) 14 | private val lock = Any() 15 | private var cefApp: CefApp? = null 16 | private var cefClient: CefClient? = null 17 | private val browsers = mutableSetOf() 18 | 19 | fun initIfNeeded() { 20 | synchronized(lock) { 21 | if (cefApp == null) { 22 | logger.info("Initializing CEF with optimized settings") 23 | 24 | System.setProperty("ide.browser.jcef.jsQueryPoolSize", "200") 25 | System.setProperty("ide.browser.jcef.gpu.disable", "false") // Enable GPU acceleration by default 26 | 27 | try { 28 | cefApp = CefApp.getInstance() 29 | cefClient = cefApp!!.createClient() 30 | logger.info("CEF initialized successfully") 31 | } catch (e: Exception) { 32 | logger.error("Failed to initialize CEF", e) 33 | throw e 34 | } 35 | } 36 | } 37 | } 38 | 39 | fun registerBrowser(browser: CefBrowser) { 40 | synchronized(lock) { 41 | browsers.add(browser) 42 | logger.info("Registered existing browser. Total browsers: ${browsers.size}") 43 | } 44 | } 45 | 46 | fun releaseBrowser(browser: CefBrowser) { 47 | synchronized(lock) { 48 | if (browsers.remove(browser)) { 49 | try { 50 | // Force close the browser 51 | browser.close(true) 52 | logger.info("Browser closed. Remaining browsers: ${browsers.size}") 53 | 54 | // If this was the last browser, clean up CEF resources 55 | if (browsers.isEmpty()) { 56 | cleanupCef() 57 | } 58 | } catch (e: Exception) { 59 | logger.warn("Error closing browser", e) 60 | } 61 | } else { 62 | // Try to release untracked browser 63 | try { 64 | browser.close(true) 65 | logger.info("Released untracked browser") 66 | } catch (e: Exception) { 67 | logger.warn("Error releasing untracked browser", e) 68 | } 69 | } 70 | } 71 | } 72 | 73 | fun getActiveBrowserCount(): Int { 74 | synchronized(lock) { 75 | return browsers.size 76 | } 77 | } 78 | 79 | private fun cleanupCef() { 80 | try { 81 | logger.info("Cleaning up CEF resources") 82 | 83 | cefClient?.dispose() 84 | cefApp?.dispose() 85 | 86 | cefClient = null 87 | cefApp = null 88 | 89 | logger.info("CEF cleanup completed") 90 | } catch (e: Exception) { 91 | logger.error("Error during CEF cleanup", e) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/io/RequestHelpers.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.io 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.JsonObject 5 | import com.smallcloud.refactai.FimCache 6 | import com.smallcloud.refactai.account.AccountManager 7 | import com.smallcloud.refactai.struct.SMCExceptions 8 | import com.smallcloud.refactai.struct.SMCRequest 9 | import com.smallcloud.refactai.struct.SMCStreamingPeace 10 | import java.util.concurrent.CompletableFuture 11 | import java.util.concurrent.Future 12 | import com.smallcloud.refactai.io.InferenceGlobalContext.Companion.instance as InferenceGlobalContext 13 | import com.smallcloud.refactai.statistic.UsageStats.Companion.instance as UsageStats 14 | 15 | private fun lookForCommonErrors(json: JsonObject, request: SMCRequest): String? { 16 | if (json.has("detail")) { 17 | val gson = Gson() 18 | val detail = gson.toJson(json.get("detail")) 19 | UsageStats?.addStatistic(false, request.stat, request.uri.toString(), detail) 20 | return detail 21 | } 22 | if (json.has("retcode") && json.get("retcode").asString != "OK") { 23 | UsageStats?.addStatistic( 24 | false, request.stat, 25 | request.uri.toString(), json.get("human_readable_message").asString 26 | ) 27 | return json.get("human_readable_message").asString 28 | } 29 | if (json.has("status") && json.get("status").asString == "error") { 30 | UsageStats?.addStatistic( 31 | false, request.stat, 32 | request.uri.toString(), json.get("human_readable_message").asString 33 | ) 34 | return json.get("human_readable_message").asString 35 | } 36 | if (json.has("error")) { 37 | UsageStats?.addStatistic( 38 | false, request.stat, 39 | request.uri.toString(), json.get("error").asJsonObject.get("message").asString 40 | ) 41 | return json.get("error").asJsonObject.get("message").asString 42 | } 43 | return null 44 | } 45 | 46 | fun streamedInferenceFetch( 47 | request: SMCRequest, 48 | dataReceiveEnded: (String) -> Unit, 49 | dataReceived: (data: SMCStreamingPeace) -> Unit = {}, 50 | ): CompletableFuture>? { 51 | val gson = Gson() 52 | val uri = request.uri 53 | val body = gson.toJson(request.body) 54 | val headers = mapOf( 55 | "Authorization" to "Bearer ${request.token}", 56 | ) 57 | 58 | val job = InferenceGlobalContext.connection.post( 59 | uri, body, headers, 60 | stat = request.stat, 61 | dataReceiveEnded = dataReceiveEnded, 62 | dataReceived = { responseBody: String, reqId: String -> 63 | val rawJson = gson.fromJson(responseBody, JsonObject::class.java) 64 | if (rawJson.has("metering_balance")) { 65 | AccountManager.instance.meteringBalance = rawJson.get("metering_balance").asInt 66 | } 67 | 68 | FimCache.maybeSendFimData(responseBody) 69 | 70 | val json = gson.fromJson(responseBody, SMCStreamingPeace::class.java) 71 | InferenceGlobalContext.lastAutoModel = json.model 72 | json.requestId = reqId 73 | UsageStats?.addStatistic(true, request.stat, request.uri.toString(), "") 74 | dataReceived(json) 75 | }, 76 | errorDataReceived = { 77 | lookForCommonErrors(it, request)?.let { message -> 78 | throw SMCExceptions(message) 79 | } 80 | }, 81 | requestId = request.id 82 | ) 83 | 84 | return job 85 | } 86 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/modes/diff/DiffLayout.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.modes.diff 2 | 3 | import com.intellij.openapi.Disposable 4 | import com.intellij.openapi.command.WriteCommandAction 5 | import com.intellij.openapi.diagnostic.Logger 6 | import com.intellij.openapi.editor.Editor 7 | import com.intellij.openapi.util.Disposer 8 | import com.smallcloud.refactai.modes.diff.renderer.Inlayer 9 | import dev.gitlive.difflib.patch.DeltaType 10 | import dev.gitlive.difflib.patch.Patch 11 | 12 | class DiffLayout( 13 | private val editor: Editor, 14 | val content: String, 15 | ) : Disposable { 16 | private var inlayer: Inlayer = Inlayer(editor, content) 17 | private var blockEvents: Boolean = false 18 | private var lastPatch = Patch() 19 | var rendered: Boolean = false 20 | 21 | override fun dispose() { 22 | rendered = false 23 | blockEvents = false 24 | inlayer.dispose() 25 | } 26 | 27 | private fun getOffsetFromStringNumber(stringNumber: Int, column: Int = 0): Int { 28 | return getOffsetFromStringNumber(editor, stringNumber, column) 29 | } 30 | 31 | fun update(patch: Patch): DiffLayout { 32 | assert(!rendered) { "Already rendered" } 33 | try { 34 | blockEvents = true 35 | editor.document.startGuardedBlockChecking() 36 | lastPatch = patch 37 | inlayer.update(patch) 38 | rendered = true 39 | } catch (ex: Exception) { 40 | Disposer.dispose(this) 41 | throw ex 42 | } finally { 43 | editor.document.stopGuardedBlockChecking() 44 | blockEvents = false 45 | } 46 | return this 47 | } 48 | 49 | fun cancelPreview() { 50 | Disposer.dispose(this) 51 | } 52 | 53 | fun applyPreview() { 54 | try { 55 | WriteCommandAction.runWriteCommandAction(editor.project!!) { 56 | applyPreviewInternal() 57 | } 58 | } catch (e: Throwable) { 59 | Logger.getInstance(javaClass).warn("Failed in the processes of accepting completion", e) 60 | } finally { 61 | Disposer.dispose(this) 62 | } 63 | } 64 | 65 | private fun applyPreviewInternal() { 66 | val document = editor.document 67 | for (det in lastPatch.getDeltas().sortedByDescending { it.source.position }) { 68 | if (det.target.lines == null) continue 69 | when (det.type) { 70 | DeltaType.INSERT -> { 71 | document.insertString( 72 | getOffsetFromStringNumber(det.source.position), 73 | det.target.lines!!.joinToString("") 74 | ) 75 | } 76 | 77 | DeltaType.CHANGE -> { 78 | document.deleteString( 79 | getOffsetFromStringNumber(det.source.position), 80 | getOffsetFromStringNumber(det.source.position + det.source.size()) 81 | ) 82 | document.insertString( 83 | getOffsetFromStringNumber(det.source.position), 84 | det.target.lines!!.joinToString("") 85 | ) 86 | } 87 | 88 | DeltaType.DELETE -> { 89 | document.deleteString( 90 | getOffsetFromStringNumber(det.source.position), 91 | getOffsetFromStringNumber(det.source.position + det.source.size()) 92 | ) 93 | } 94 | 95 | else -> {} 96 | } 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/listeners/AcceptAction.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.listeners 2 | 3 | 4 | import com.intellij.codeInsight.hint.HintManagerImpl.ActionToIgnore 5 | import com.intellij.codeInsight.inline.completion.InlineCompletion 6 | import com.intellij.codeInsight.inline.completion.session.InlineCompletionContext 7 | import com.intellij.openapi.actionSystem.DataContext 8 | import com.intellij.openapi.components.service 9 | import com.intellij.openapi.diagnostic.Logger 10 | import com.intellij.openapi.editor.Caret 11 | import com.intellij.openapi.editor.Editor 12 | import com.intellij.openapi.editor.actionSystem.EditorAction 13 | import com.intellij.openapi.editor.actionSystem.EditorWriteActionHandler 14 | import com.intellij.openapi.util.TextRange 15 | import com.smallcloud.refactai.Resources 16 | import com.smallcloud.refactai.codecompletion.EditorRefactLastCompletionIsMultilineKey 17 | import com.smallcloud.refactai.codecompletion.EditorRefactLastSnippetTelemetryIdKey 18 | import com.smallcloud.refactai.codecompletion.InlineCompletionGrayTextElementCustom 19 | import com.smallcloud.refactai.modes.ModeProvider 20 | import com.smallcloud.refactai.statistic.UsageStats 21 | import kotlin.math.absoluteValue 22 | 23 | const val ACTION_ID_ = "TabPressedAction" 24 | 25 | class TabPressedAction : EditorAction(InsertInlineCompletionHandler()), ActionToIgnore { 26 | val ACTION_ID = ACTION_ID_ 27 | 28 | init { 29 | this.templatePresentation.icon = Resources.Icons.LOGO_RED_16x16 30 | } 31 | 32 | class InsertInlineCompletionHandler : EditorWriteActionHandler() { 33 | override fun executeWriteAction(editor: Editor, caret: Caret?, dataContext: DataContext) { 34 | Logger.getInstance("RefactTabPressedAction").debug("executeWriteAction") 35 | val provider = ModeProvider.getOrCreateModeProvider(editor) 36 | if (provider.isInCompletionMode()) { 37 | InlineCompletion.getHandlerOrNull(editor)?.insert() 38 | EditorRefactLastSnippetTelemetryIdKey[editor]?.also { 39 | editor.project?.service()?.snippetAccepted(it) 40 | EditorRefactLastSnippetTelemetryIdKey[editor] = null 41 | EditorRefactLastCompletionIsMultilineKey[editor] = null 42 | } 43 | } else { 44 | provider.onTabPressed(editor, caret, dataContext) 45 | } 46 | } 47 | 48 | override fun isEnabledForCaret( 49 | editor: Editor, 50 | caret: Caret, 51 | dataContext: DataContext 52 | ): Boolean { 53 | val provider = ModeProvider.getOrCreateModeProvider(editor) 54 | if (provider.isInCompletionMode()) { 55 | val ctx = InlineCompletionContext.getOrNull(editor) ?: return false 56 | if (ctx.state.elements.isEmpty()) return false 57 | val elem = ctx.state.elements.first() 58 | val isMultiline = EditorRefactLastCompletionIsMultilineKey[editor] 59 | if (isMultiline && elem is InlineCompletionGrayTextElementCustom.Presentable) { 60 | val startOffset = elem.startOffset() ?: return false 61 | if (elem.delta == 0) 62 | return elem.delta == (caret.offset - startOffset).absoluteValue 63 | else { 64 | val prefixOffset = editor.document.getLineStartOffset(caret.logicalPosition.line) 65 | return elem.delta == (caret.offset - prefixOffset) 66 | } 67 | } 68 | return true 69 | } else { 70 | return ModeProvider.getOrCreateModeProvider(editor).modeInActiveState() 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/PluginErrorReportSubmitter.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai 2 | 3 | import com.intellij.ide.BrowserUtil 4 | import com.intellij.openapi.Disposable 5 | import com.intellij.openapi.application.ex.ApplicationInfoEx 6 | import com.intellij.openapi.diagnostic.ErrorReportSubmitter 7 | import com.intellij.openapi.diagnostic.IdeaLoggingEvent 8 | import com.intellij.openapi.diagnostic.SubmittedReportInfo 9 | import com.intellij.openapi.util.SystemInfo 10 | import com.intellij.util.Consumer 11 | import com.smallcloud.refactai.lsp.LSPProcessHolder.Companion.buildInfo 12 | import com.smallcloud.refactai.struct.DeploymentMode 13 | import java.awt.Component 14 | import java.net.URLEncoder 15 | import com.smallcloud.refactai.io.InferenceGlobalContext.Companion.instance as InferenceGlobalContext 16 | 17 | private fun String.urlEncoded(): String = URLEncoder.encode(this, "UTF-8") 18 | 19 | class PluginErrorReportSubmitter : ErrorReportSubmitter(), Disposable { 20 | override fun submit( 21 | events: Array, 22 | additionalInfo: String?, 23 | parentComponent: Component, 24 | consumer: Consumer 25 | ): Boolean { 26 | val event = events.firstOrNull() 27 | val eventMessage = event?.message ?: "(no message)" 28 | val eventThrowable = if (event?.throwableText == null) { 29 | if (event?.throwableText?.length!! > 9_000) { 30 | event.throwableText.slice(0..8_997) + "..." 31 | } else { 32 | event.throwableText 33 | } 34 | } else { 35 | "(no stack trace)" 36 | } 37 | val exceptionClassName = event.throwableText?.lines()?.firstOrNull()?.split(':')?.firstOrNull()?.split('.')?.lastOrNull()?.let { ": $it" }.orEmpty() 38 | val issueTitle = "[JB plugin] Internal error${exceptionClassName}".urlEncoded() 39 | val ideNameAndVersion = ApplicationInfoEx.getInstanceEx().let { appInfo -> 40 | appInfo.fullApplicationName + " " + "Build #" + appInfo.build.asString() 41 | } 42 | val mode = when(InferenceGlobalContext.deploymentMode) { 43 | DeploymentMode.CLOUD -> "Cloud" 44 | DeploymentMode.SELF_HOSTED -> "Self-Hosted/Enterprise" 45 | DeploymentMode.HF -> "HF" 46 | } 47 | val pluginVersion = getThisPlugin()?.version ?: "unknown" 48 | val properties = System.getProperties() 49 | val jdk = properties.getProperty("java.version", "unknown") + 50 | "; VM: " + properties.getProperty("java.vm.name", "unknown") + 51 | "; Vendor: " + properties.getProperty("java.vendor", "unknown") 52 | val os = SystemInfo.getOsNameAndVersion() 53 | val arch = SystemInfo.OS_ARCH 54 | val issueBody = """ 55 | |An internal error happened in the IDE plugin. 56 | | 57 | |Message: $eventMessage 58 | | 59 | |### Stack trace 60 | |``` 61 | |$eventThrowable 62 | |``` 63 | | 64 | |### Environment 65 | |- Plugin version: $pluginVersion 66 | |- IDE: $ideNameAndVersion 67 | |- JDK: $jdk 68 | |- OS: $os 69 | |- ARCH: $arch 70 | |- MODE: $mode 71 | |- LSP BUILD INFO: $buildInfo 72 | | 73 | |### Additional information 74 | |${additionalInfo.orEmpty()} 75 | """.trimMargin().urlEncoded() 76 | val gitHubUrl = "https://github.com/smallcloudai/refact-intellij/issues/new?" + 77 | "labels=bug" + 78 | "&title=${issueTitle}" + 79 | "&body=${issueBody}" 80 | BrowserUtil.browse(gitHubUrl) 81 | consumer.consume(SubmittedReportInfo(SubmittedReportInfo.SubmissionStatus.NEW_ISSUE)) 82 | return true 83 | } 84 | override fun getReportActionText() = RefactAIBundle.message("errorReport.actionText") 85 | 86 | override fun dispose() {} 87 | } -------------------------------------------------------------------------------- /src/main/resources/icons/coin_16x16.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/Resources.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai 2 | 3 | import com.intellij.ide.plugins.IdeaPluginDescriptor 4 | import com.intellij.ide.plugins.PluginManagerCore 5 | import com.intellij.openapi.application.ApplicationInfo 6 | import com.intellij.openapi.extensions.PluginId 7 | import com.intellij.openapi.util.IconLoader 8 | import com.intellij.openapi.util.SystemInfo 9 | import com.intellij.util.IconUtil 10 | import java.io.File 11 | import java.net.URI 12 | import javax.swing.Icon 13 | import javax.swing.UIManager 14 | 15 | 16 | fun getThisPlugin(): IdeaPluginDescriptor? { 17 | val thisPluginById = PluginManagerCore.getPlugin(PluginId.getId("com.smallcloud.codify")) 18 | if (thisPluginById != null) { 19 | return thisPluginById 20 | } 21 | return null 22 | } 23 | 24 | 25 | private fun getHomePath(): File { 26 | return getThisPlugin()!!.pluginPath.toFile() 27 | } 28 | 29 | private fun getVersion(): String { 30 | val thisPlugin = getThisPlugin() 31 | if (thisPlugin != null) { 32 | return thisPlugin.version 33 | } 34 | return "" 35 | } 36 | 37 | 38 | private fun getPluginId(): PluginId { 39 | val thisPlugin = getThisPlugin() 40 | if (thisPlugin != null) { 41 | return thisPlugin.pluginId 42 | } 43 | return PluginId.getId("com.smallcloud.codify") 44 | } 45 | 46 | private fun getArch(): String { 47 | val arch = SystemInfo.OS_ARCH 48 | return when (arch) { 49 | "amd64" -> "x86_64" 50 | "aarch64" -> "aarch64" 51 | else -> arch 52 | } 53 | } 54 | 55 | private fun getBinPrefix(): String { 56 | var suffix = "" 57 | if (SystemInfo.isMac) { 58 | suffix = "apple-darwin" 59 | } else if (SystemInfo.isWindows) { 60 | suffix = "pc-windows-msvc" 61 | } else if (SystemInfo.isLinux) { 62 | suffix = "unknown-linux-gnu" 63 | } 64 | 65 | return "dist-${getArch()}-${suffix}" 66 | } 67 | 68 | object Resources { 69 | val binPrefix: String = getBinPrefix() 70 | 71 | val defaultCloudUrl: URI = URI("https://www.smallcloud.ai") 72 | val defaultCodeCompletionUrlSuffix = URI("v1/code-completion") 73 | val cloudUserMessage: URI = defaultCloudUrl.resolve("/v1/user-message") 74 | val defaultReportUrlSuffix: URI = URI("v1/telemetry-network") 75 | val defaultChatReportUrlSuffix: URI = URI("v1/telemetry-chat") 76 | val defaultSnippetAcceptedUrlSuffix: URI = URI("v1/snippet-accepted") 77 | val version: String = getVersion() 78 | const val client: String = "jetbrains" 79 | const val titleStr: String = "Refact.ai" 80 | val pluginId: PluginId = getPluginId() 81 | val jbBuildVersion: String = ApplicationInfo.getInstance().build.toString() 82 | const val refactAIRootSettingsID = "refactai_root" 83 | const val refactAIAdvancedSettingsID = "refactai_advanced_settings" 84 | 85 | object Icons { 86 | private fun brushForTheme(icon: Icon): Icon { 87 | return if (UIManager.getLookAndFeel().name.contains("Darcula")) { 88 | IconUtil.brighter(icon, 3) 89 | } else { 90 | IconUtil.darker(icon, 3) 91 | } 92 | } 93 | 94 | private fun makeIcon(path: String): Icon { 95 | return brushForTheme(IconLoader.getIcon(path, Resources::class.java)) 96 | } 97 | 98 | val LOGO_RED_12x12: Icon = IconLoader.getIcon("/icons/refactai_logo_red_12x12.svg", Resources::class.java) 99 | val LOGO_RED_13x13: Icon = IconLoader.getIcon("/icons/refactai_logo_red_13x13.svg", Resources::class.java) 100 | val LOGO_12x12: Icon = makeIcon("/icons/refactai_logo_12x12.svg") 101 | val LOGO_RED_16x16: Icon = IconLoader.getIcon("/icons/refactai_logo_red_16x16.svg", Resources::class.java) 102 | 103 | val COIN_16x16: Icon = makeIcon("/icons/coin_16x16.svg") 104 | val HAND_12x12: Icon = makeIcon("/icons/hand_12x12.svg") 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/utils/AsyncMessageHandler.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.utils 2 | 3 | import com.intellij.openapi.Disposable 4 | import com.intellij.openapi.application.ApplicationManager 5 | import com.intellij.openapi.diagnostic.Logger 6 | import java.util.concurrent.ArrayBlockingQueue 7 | import java.util.concurrent.Executors 8 | import java.util.concurrent.atomic.AtomicBoolean 9 | 10 | /** 11 | * Handles JavaScript to Kotlin message processing asynchronously to prevent blocking CEF I/O threads. 12 | * Messages are queued, parsed on a background thread, then dispatched on the EDT. 13 | */ 14 | class AsyncMessageHandler( 15 | private val parser: (String) -> T?, 16 | private val dispatcher: (T) -> Unit, 17 | queueSize: Int = 1000 18 | ) : Disposable { 19 | 20 | private val logger = Logger.getInstance(AsyncMessageHandler::class.java) 21 | private val messageQueue = ArrayBlockingQueue(queueSize) 22 | private val executor = Executors.newSingleThreadExecutor { runnable: Runnable -> 23 | Thread(runnable, "SMC-AsyncMessageHandler").apply { 24 | isDaemon = true 25 | } 26 | } 27 | private val disposed = AtomicBoolean(false) 28 | 29 | init { 30 | startMessageProcessor() 31 | } 32 | 33 | fun offerMessage(rawMessage: String): Boolean { 34 | if (disposed.get()) { 35 | logger.warn("Attempted to offer message to disposed handler") 36 | return false 37 | } 38 | 39 | val offered = messageQueue.offer(rawMessage) 40 | if (!offered) { 41 | logger.warn("Message queue full, dropping message: ${rawMessage.take(100)}...") 42 | } 43 | 44 | return offered 45 | } 46 | 47 | fun getQueueSize(): Int = messageQueue.size 48 | 49 | private fun startMessageProcessor() { 50 | executor.submit { 51 | logger.info("AsyncMessageHandler started") 52 | 53 | while (!disposed.get() && !Thread.currentThread().isInterrupted) { 54 | try { 55 | // Take message from queue (blocks until available) 56 | val rawMessage = messageQueue.take() 57 | 58 | // Parse message on background thread 59 | val parsedMessage = try { 60 | parser(rawMessage) 61 | } catch (e: Exception) { 62 | logger.warn("Error parsing message: ${rawMessage.take(100)}...", e) 63 | null 64 | } 65 | 66 | // Dispatch on EDT if parsing succeeded 67 | if (parsedMessage != null) { 68 | ApplicationManager.getApplication().invokeLater { 69 | if (!disposed.get()) { 70 | try { 71 | dispatcher(parsedMessage) 72 | } catch (e: Exception) { 73 | logger.warn("Error dispatching parsed message", e) 74 | } 75 | } 76 | } 77 | } 78 | 79 | } catch (e: InterruptedException) { 80 | logger.info("AsyncMessageHandler interrupted") 81 | break 82 | } catch (e: Exception) { 83 | logger.error("Unexpected error in message processor", e) 84 | } 85 | } 86 | 87 | logger.info("AsyncMessageHandler stopped") 88 | } 89 | } 90 | 91 | override fun dispose() { 92 | if (disposed.compareAndSet(false, true)) { 93 | logger.info("Disposing AsyncMessageHandler with ${messageQueue.size} pending messages") 94 | messageQueue.clear() 95 | executor.shutdownNow() 96 | logger.info("AsyncMessageHandler disposal completed") 97 | } 98 | } 99 | 100 | fun isDisposed(): Boolean = disposed.get() 101 | } 102 | -------------------------------------------------------------------------------- /src/test/kotlin/com/smallcloud/refactai/lsp/LSPProcessHolderTest.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.lsp 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.serviceContainer.AlreadyDisposedException 5 | import com.intellij.testFramework.fixtures.BasePlatformTestCase 6 | import com.intellij.util.concurrency.AppExecutorUtil 7 | import com.intellij.util.messages.MessageBus 8 | import org.junit.Test 9 | import org.mockito.Mockito 10 | import org.mockito.Mockito.`when` 11 | import org.mockito.Mockito.mock 12 | import java.util.concurrent.CountDownLatch 13 | import java.util.concurrent.TimeUnit 14 | 15 | /** 16 | * Test that demonstrates the "Already disposed" issue in LSPProcessHolder. 17 | * This reproduces the specific AlreadyDisposedException from GitHub issue #155. 18 | */ 19 | class LSPProcessHolderTest : BasePlatformTestCase() { 20 | 21 | class TestLspProccessHolder(project: Project) : LSPProcessHolder(project) { 22 | 23 | // Latch to control test execution flow 24 | private val latch = CountDownLatch(1) 25 | 26 | // Method to simulate the race condition that causes the issue in GitHub #155 27 | fun simulateRaceConditionWithScheduledTask(makeProjectDisposed: () -> Unit): AlreadyDisposedException? { 28 | var caughtException: AlreadyDisposedException? = null 29 | 30 | // Schedule a task that will set capabilities (similar to what happens in startProcess()) 31 | val future = AppExecutorUtil.getAppScheduledExecutorService().submit { 32 | try { 33 | latch.await(1, TimeUnit.SECONDS) 34 | capabilities = LSPCapabilities(cloudName = "test-cloud") 35 | } catch (e: Exception) { 36 | if (e is AlreadyDisposedException) { 37 | caughtException = e 38 | } 39 | println("Exception in scheduled task: ${e.javaClass.name}: ${e.message}") 40 | } 41 | } 42 | 43 | makeProjectDisposed() 44 | latch.countDown() 45 | future.get(2, TimeUnit.SECONDS) 46 | 47 | return caughtException 48 | } 49 | 50 | // Override startProcess to reproduce the exact call stack from the issue 51 | fun simulateStartProcess() { 52 | // This simulates the call to setCapabilities from startProcess() in LSPProcessHolder 53 | capabilities = LSPCapabilities(cloudName = "test-cloud") 54 | } 55 | } 56 | 57 | @Test 58 | fun testAlreadyDisposedException() { 59 | // Create mock objects 60 | val mockProject = mock(Project::class.java) 61 | val mockMessageBus = mock(MessageBus::class.java) 62 | val mockPublisher = mock(LSPProcessHolderChangedNotifier::class.java) 63 | 64 | // Set up the mock project 65 | `when`(mockProject.isDisposed).thenReturn(false) 66 | `when`(mockProject.messageBus).thenReturn(mockMessageBus) 67 | 68 | // Set up the mock message bus 69 | `when`(mockMessageBus.syncPublisher(LSPProcessHolderChangedNotifier.TOPIC)).thenReturn(mockPublisher) 70 | 71 | // Create the test holder 72 | val holder = TestLspProccessHolder(mockProject) 73 | 74 | 75 | // Make project disposed and try to access messageBus in a scheduled task 76 | val exception = holder.simulateRaceConditionWithScheduledTask { 77 | `when`(mockProject.isDisposed).thenReturn(true) 78 | // When project is disposed, accessing messageBus should throw AlreadyDisposedException 79 | // But with the fix, we never access messageBus when project is disposed 80 | `when`(mockProject.messageBus).thenThrow( 81 | AlreadyDisposedException("Already disposed") 82 | ) 83 | } 84 | 85 | // With the fix, no exception should be thrown 86 | assertNull("With the fix, no AlreadyDisposedException should be thrown", exception) 87 | // Verify that the capabilities were still set correctly 88 | assertEquals("test-cloud", holder.capabilities.cloudName) 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/modes/diff/DiffMode.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.modes.diff 2 | 3 | import com.intellij.openapi.actionSystem.DataContext 4 | import com.intellij.openapi.application.ApplicationManager 5 | import com.intellij.openapi.editor.Caret 6 | import com.intellij.openapi.editor.Editor 7 | import com.intellij.openapi.editor.event.CaretEvent 8 | import com.smallcloud.refactai.modes.Mode 9 | import com.smallcloud.refactai.modes.ModeProvider.Companion.getOrCreateModeProvider 10 | import com.smallcloud.refactai.modes.ModeType 11 | import com.smallcloud.refactai.modes.completion.structs.DocumentEventExtra 12 | import dev.gitlive.difflib.DiffUtils 13 | 14 | open class DiffMode( 15 | override var needToRender: Boolean = true 16 | ) : Mode { 17 | private val app = ApplicationManager.getApplication() 18 | private var diffLayout: DiffLayout? = null 19 | 20 | 21 | private fun cancel(editor: Editor?) { 22 | app.invokeLater { 23 | diffLayout?.cancelPreview() 24 | diffLayout = null 25 | } 26 | if (editor != null && !Thread.currentThread().stackTrace.any { it.methodName == "switchMode" }) { 27 | getOrCreateModeProvider(editor).switchMode() 28 | } 29 | } 30 | 31 | override fun beforeDocumentChangeNonBulk(event: DocumentEventExtra) { 32 | cancel(event.editor) 33 | } 34 | 35 | override fun onTextChange(event: DocumentEventExtra) { 36 | } 37 | 38 | override fun onTabPressed(editor: Editor, caret: Caret?, dataContext: DataContext) { 39 | diffLayout?.applyPreview() 40 | diffLayout = null 41 | } 42 | 43 | override fun onEscPressed(editor: Editor, caret: Caret?, dataContext: DataContext) { 44 | cancel(editor) 45 | } 46 | 47 | override fun onCaretChange(event: CaretEvent) {} 48 | 49 | fun isInRenderState(): Boolean { 50 | return (diffLayout != null && !diffLayout!!.rendered) 51 | } 52 | 53 | override fun isInActiveState(): Boolean { 54 | return isInRenderState() || diffLayout != null 55 | } 56 | 57 | override fun show() { 58 | TODO("Not yet implemented") 59 | } 60 | 61 | override fun hide() { 62 | TODO("Not yet implemented") 63 | } 64 | 65 | override fun cleanup(editor: Editor) { 66 | cancel(editor) 67 | } 68 | 69 | fun actionPerformed( 70 | editor: Editor, 71 | content: String, 72 | modeType: ModeType = ModeType.Diff 73 | ) { 74 | val selectionModel = editor.selectionModel 75 | val startSelectionOffset: Int = selectionModel.selectionStart 76 | val endSelectionOffset: Int = selectionModel.selectionEnd 77 | 78 | val indent = selectionModel.selectedText?.takeWhile { it ==' ' || it == '\t' } 79 | val indentedCode = content.prependIndent(indent?: "") 80 | 81 | selectionModel.removeSelection() 82 | // doesn't seem to take focus 83 | // editor.contentComponent.requestFocus() 84 | getOrCreateModeProvider(editor).switchMode(modeType) 85 | diffLayout?.cancelPreview() 86 | val diff = DiffLayout(editor, content) 87 | val originalText = editor.document.text 88 | val newText = originalText.replaceRange(startSelectionOffset, endSelectionOffset, indentedCode) 89 | val patch = DiffUtils.diff(originalText.split("(?<=\n)".toRegex()), newText.split("(?<=\n)".toRegex())) 90 | 91 | diffLayout = diff.update(patch) 92 | 93 | app.invokeLater { 94 | editor.contentComponent.requestFocusInWindow() 95 | } 96 | } 97 | } 98 | 99 | class DiffModeWithSideEffects( 100 | var onTab: (editor: Editor, caret: Caret?, dataContext: DataContext) -> Unit, 101 | var onEsc: (editor: Editor, caret: Caret?, dataContext: DataContext) -> Unit 102 | ) : DiffMode() { 103 | 104 | override fun onTabPressed(editor: Editor, caret: Caret?, dataContext: DataContext) { 105 | super.onTabPressed(editor, caret, dataContext) 106 | onTab(editor, caret, dataContext) 107 | } 108 | 109 | override fun onEscPressed(editor: Editor, caret: Caret?, dataContext: DataContext) { 110 | super.onEscPressed(editor, caret, dataContext) 111 | onEsc(editor, caret, dataContext) 112 | } 113 | 114 | fun actionPerformed(editor: Editor, content: String) { 115 | super.actionPerformed(editor, content, ModeType.DiffWithSideEffects) 116 | } 117 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/UpdateChecker.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import com.google.gson.Gson 6 | import com.intellij.ide.plugins.marketplace.IdeCompatibleUpdate 7 | import com.intellij.notification.Notification 8 | import com.intellij.notification.NotificationAction 9 | import com.intellij.notification.NotificationGroupManager 10 | import com.intellij.notification.NotificationType 11 | import com.intellij.openapi.Disposable 12 | import com.intellij.openapi.application.ApplicationInfo 13 | import com.intellij.openapi.application.ApplicationManager 14 | import com.intellij.openapi.options.ShowSettingsUtil 15 | import com.intellij.util.Urls 16 | import com.intellij.util.concurrency.AppExecutorUtil 17 | import com.intellij.util.io.HttpRequests 18 | import com.smallcloud.refactai.utils.getLastUsedProject 19 | import java.util.concurrent.Future 20 | import java.util.concurrent.TimeUnit 21 | 22 | class UpdateChecker : Disposable { 23 | private val scheduler = AppExecutorUtil.createBoundedScheduledExecutorService( 24 | "SMCUpdateCheckerScheduler", 1 25 | ) 26 | private var task: Future<*>? = null 27 | private var notification: Notification? = null 28 | // ApplicationInfoImpl.DEFAULT_PLUGINS_HOST 29 | private var DEFAULT_PLUGINS_HOST: String = "https://plugins.jetbrains.com" 30 | 31 | init { 32 | task = scheduler.schedule({ 33 | checkNewVersion() 34 | }, 1, TimeUnit.MINUTES) 35 | } 36 | 37 | private fun checkNewVersion() { 38 | val objectMapper = ObjectMapper() 39 | 40 | val data = Gson().toJson( 41 | mapOf( 42 | "build" to ApplicationInfo.getInstance().build.asString(), 43 | "pluginXMLIds" to listOf(Resources.pluginId.idString) 44 | ) 45 | ) 46 | 47 | val thisPlugin = getThisPlugin() ?: return 48 | 49 | try { 50 | val newVersions = HttpRequests 51 | .post( 52 | Urls.newFromEncoded("${DEFAULT_PLUGINS_HOST}/api/search/compatibleUpdates").toExternalForm(), 53 | HttpRequests.JSON_CONTENT_TYPE 54 | ) 55 | .productNameAsUserAgent() 56 | .throwStatusCodeException(false) 57 | .connect { 58 | it.write(data) 59 | objectMapper.readValue(it.inputStream, object : TypeReference>() {}) 60 | } 61 | if (newVersions.isEmpty()) { 62 | return 63 | } 64 | val thisNewPlugin = newVersions.find { it.pluginId == Resources.pluginId.idString } ?: return 65 | 66 | if (thisNewPlugin.version > thisPlugin.version) { 67 | emitUpdate(thisNewPlugin.version) 68 | } 69 | } catch (_: Exception) { 70 | // do nothing 71 | } 72 | } 73 | 74 | private fun emitUpdate(newVersion: String) { 75 | notification?.apply { 76 | expire() 77 | hideBalloon() 78 | } 79 | 80 | val project = getLastUsedProject() 81 | val notification = NotificationGroupManager 82 | .getInstance() 83 | .getNotificationGroup("Refact AI Notification Group") 84 | .createNotification( 85 | Resources.titleStr, 86 | RefactAIBundle.message("updateChecker.newVersionIsAvailable", newVersion), 87 | NotificationType.INFORMATION 88 | ) 89 | notification.icon = Resources.Icons.LOGO_RED_16x16 90 | 91 | notification.addAction(NotificationAction.createSimple( 92 | RefactAIBundle.message("updateChecker.update") 93 | ) { 94 | ShowSettingsUtil.getInstance().showSettingsDialog( 95 | project, 96 | "Plugins" 97 | ) 98 | notification.expire() 99 | }) 100 | notification.notify(project) 101 | this.notification = notification 102 | } 103 | 104 | 105 | companion object { 106 | @JvmStatic 107 | val instance: UpdateChecker 108 | get() = ApplicationManager.getApplication().getService(UpdateChecker::class.java) 109 | } 110 | 111 | override fun dispose() { 112 | task?.cancel(true) 113 | scheduler.shutdown() 114 | } 115 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/code_lens/CodeLensAction.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.code_lens 2 | 3 | import com.intellij.openapi.actionSystem.AnActionEvent 4 | import com.intellij.openapi.application.ApplicationManager 5 | import com.intellij.openapi.components.service 6 | import com.intellij.openapi.editor.Editor 7 | import com.intellij.openapi.editor.LogicalPosition 8 | import com.intellij.openapi.project.DumbAwareAction 9 | import com.intellij.openapi.roots.ProjectRootManager 10 | import com.intellij.openapi.wm.ToolWindowManager 11 | import com.smallcloud.refactai.Resources 12 | import com.smallcloud.refactai.panes.RefactAIToolboxPaneFactory 13 | import com.smallcloud.refactai.statistic.UsageStatistic 14 | import com.smallcloud.refactai.statistic.UsageStats 15 | import com.smallcloud.refactai.struct.ChatMessage 16 | import java.util.concurrent.atomic.AtomicBoolean 17 | import kotlin.io.path.relativeTo 18 | 19 | class CodeLensAction( 20 | private val editor: Editor, 21 | private val line1: Int, 22 | private val line2: Int, 23 | private val messages: Array, 24 | private val sendImmediately: Boolean, 25 | private val openNewTab: Boolean 26 | ) : DumbAwareAction(Resources.Icons.LOGO_RED_16x16) { 27 | override fun actionPerformed(p0: AnActionEvent) { 28 | actionPerformed() 29 | } 30 | 31 | private fun replaceVariablesInText( 32 | text: String, 33 | relativePath: String, 34 | cursor: Int?, 35 | codeSelection: String 36 | ): String { 37 | return text 38 | .replace("%CURRENT_FILE%", relativePath) 39 | .replace("%CURSOR_LINE%", cursor?.plus(1)?.toString() ?: "") 40 | .replace("%CODE_SELECTION%", codeSelection) 41 | .replace("%PROMPT_EXPLORATION_TOOLS%", "") 42 | } 43 | 44 | private fun formatMultipleMessagesForCodeLens( 45 | messages: Array, 46 | relativePath: String, 47 | cursor: Int?, 48 | text: String 49 | ): Array { 50 | val formattedMessages = messages.map { message -> 51 | if (message.role == "user") { 52 | message.copy( 53 | content = replaceVariablesInText(message.content, relativePath, cursor, text) 54 | ) 55 | } else { 56 | message 57 | } 58 | }.toTypedArray() 59 | return formattedMessages 60 | } 61 | 62 | private fun formatMessages(): Array { 63 | val pos1 = LogicalPosition(line1, 0) 64 | val text = editor.document.text.slice( 65 | editor.logicalPositionToOffset(pos1) until editor.document.getLineEndOffset(line2) 66 | ) 67 | val filePath = editor.virtualFile.toNioPath() 68 | val relativePath = editor.project?.let { 69 | ProjectRootManager.getInstance(it).contentRoots.map { root -> 70 | filePath.relativeTo(root.toNioPath()) 71 | }.minBy { it.toString().length } 72 | } 73 | 74 | val formattedMessages = formatMultipleMessagesForCodeLens(messages, relativePath?.toString() ?: filePath.toString(), line1, text); 75 | 76 | return formattedMessages 77 | } 78 | 79 | private val isActionRunning = AtomicBoolean(false) 80 | 81 | fun actionPerformed() { 82 | val chat = editor.project?.let { ToolWindowManager.getInstance(it).getToolWindow("Refact") } 83 | 84 | chat?.activate { 85 | RefactAIToolboxPaneFactory.chat?.requestFocus() 86 | RefactAIToolboxPaneFactory.chat?.executeCodeLensCommand(formatMessages(), sendImmediately, openNewTab) 87 | editor.project?.service()?.addChatStatistic(true, UsageStatistic("openChatByCodelens"), "") 88 | } 89 | 90 | // If content is empty, then it's "Open Chat" instruction, selecting range of code in active tab 91 | if (messages.isEmpty() && isActionRunning.compareAndSet(false, true)) { 92 | ApplicationManager.getApplication().invokeLater { 93 | try { 94 | val pos1 = LogicalPosition(line1, 0) 95 | val pos2 = LogicalPosition(line2, editor.document.getLineEndOffset(line2)) 96 | 97 | val intendedStart = editor.logicalPositionToOffset(pos1) 98 | val intendedEnd = editor.logicalPositionToOffset(pos2) 99 | editor.selectionModel.setSelection(intendedStart, intendedEnd) 100 | } finally { 101 | isActionRunning.set(false) 102 | } 103 | } 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/smallcloud/refactai/testUtils/MockServer.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.testUtils 2 | 3 | import com.intellij.testFramework.LightPlatform4TestCase 4 | import okhttp3.mockwebserver.MockWebServer 5 | import java.security.KeyPairGenerator 6 | import java.security.KeyStore 7 | import java.security.PrivateKey 8 | import java.security.PublicKey 9 | import java.security.cert.X509Certificate 10 | import java.security.SecureRandom 11 | import java.util.Date 12 | import javax.security.auth.x500.X500Principal 13 | import javax.net.ssl.KeyManagerFactory 14 | import javax.net.ssl.SSLContext 15 | import org.bouncycastle.cert.X509v3CertificateBuilder 16 | import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter 17 | import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder 18 | import org.bouncycastle.operator.ContentSigner 19 | import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder 20 | import org.bouncycastle.asn1.x500.X500Name 21 | import org.junit.After 22 | import org.junit.Before 23 | import java.math.BigInteger 24 | 25 | 26 | fun createSelfSignedCertificate(): Pair { 27 | val keyPairGenerator = KeyPairGenerator.getInstance("RSA") 28 | keyPairGenerator.initialize(2048) 29 | val keyPair = keyPairGenerator.generateKeyPair() 30 | val privateKey = keyPair.private 31 | val publicKey: PublicKey = keyPair.public 32 | 33 | val subject = X500Principal("CN=localhost") 34 | val issuer = subject 35 | val serialNumber = SecureRandom().nextInt().toLong() 36 | val notBefore = Date(System.currentTimeMillis()) 37 | val notAfter = Date(System.currentTimeMillis() + 365 * 24 * 60 * 60 * 1000) // 1 year validity 38 | 39 | val certificate = generateSelfSignedCertificate(subject, issuer, serialNumber, notBefore, notAfter, publicKey, privateKey) 40 | 41 | // Create a KeyStore and load the self-signed certificate 42 | val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) 43 | keyStore.load(null, null) 44 | keyStore.setKeyEntry("selfsigned", privateKey, "password".toCharArray(), arrayOf(certificate)) 45 | 46 | // Create SSLContext 47 | val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) 48 | keyManagerFactory.init(keyStore, "password".toCharArray()) 49 | 50 | val sslContext = SSLContext.getInstance("TLS") 51 | sslContext.init(keyManagerFactory.keyManagers, null, null) 52 | 53 | return Pair(sslContext, privateKey) 54 | } 55 | 56 | fun generateSelfSignedCertificate( 57 | subject: X500Principal, 58 | issuer: X500Principal, 59 | serialNumber: Long, 60 | notBefore: Date, 61 | notAfter: Date, 62 | publicKey: PublicKey, 63 | privateKey: PrivateKey 64 | ): X509Certificate { 65 | val subjectName = X500Name(subject.name) 66 | val issuerName = X500Name(issuer.name) 67 | 68 | val certBuilder: X509v3CertificateBuilder = JcaX509v3CertificateBuilder( 69 | issuerName, 70 | BigInteger.valueOf(serialNumber), 71 | notBefore, 72 | notAfter, 73 | subjectName, 74 | publicKey 75 | ) 76 | 77 | val contentSigner: ContentSigner = JcaContentSignerBuilder("SHA256WithRSA").build(privateKey) 78 | 79 | val certificate: X509Certificate = JcaX509CertificateConverter().getCertificate(certBuilder.build(contentSigner)) 80 | 81 | return certificate 82 | } 83 | 84 | 85 | abstract class MockServer: LightPlatform4TestCase() { 86 | lateinit var server: MockWebServer 87 | lateinit var baseUrl: String 88 | 89 | @Before 90 | fun setup() { 91 | server = MockWebServer() 92 | server.useHttps(sslContext.socketFactory, false) 93 | server.start() 94 | baseUrl = server.url("/").toString() 95 | } 96 | 97 | @After 98 | fun cleanup() { 99 | server.shutdown() 100 | } 101 | 102 | companion object { 103 | private var _sslContext: SSLContext? = null 104 | private var _privateKey: PrivateKey? = null 105 | 106 | val sslContext: SSLContext 107 | get() { 108 | if (_sslContext == null) { 109 | val (context, key) = createSelfSignedCertificate() 110 | _sslContext = context 111 | _privateKey = key 112 | } 113 | return _sslContext!! 114 | } 115 | 116 | val privateKey: PrivateKey 117 | get() { 118 | if (_privateKey == null) { 119 | sslContext // This will trigger the initialization 120 | } 121 | return _privateKey!! 122 | } 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /src/test/kotlin/com/smallcloud/refactai/testUtils/TestableChatWebView.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.testUtils 2 | 3 | import com.intellij.openapi.Disposable 4 | import com.smallcloud.refactai.panes.sharedchat.Events 5 | import java.util.concurrent.CountDownLatch 6 | import java.util.concurrent.TimeUnit 7 | import java.util.concurrent.atomic.AtomicBoolean 8 | import java.util.concurrent.atomic.AtomicInteger 9 | import javax.swing.JComponent 10 | import javax.swing.JPanel 11 | 12 | /** 13 | * Testable version of ChatWebView that works without JCEF initialization. 14 | * Used for unit testing the core ChatWebView functionality. 15 | */ 16 | class TestableChatWebView( 17 | private val messageHandler: (event: Events.FromChat) -> Unit 18 | ) : Disposable { 19 | 20 | private val initializationLatch = CountDownLatch(1) 21 | private val disposalLatch = CountDownLatch(1) 22 | private val _isDisposed = AtomicBoolean(false) 23 | private val isInitialized = AtomicBoolean(false) 24 | 25 | // Mock component instead of real browser 26 | private val mockComponent = JPanel() 27 | private val componentValid = AtomicBoolean(true) 28 | 29 | // Test tracking properties 30 | var messageCount = AtomicInteger(0) 31 | var styleUpdateCount = AtomicInteger(0) 32 | 33 | // Public properties for tests 34 | val isDisposed: Boolean get() = _isDisposed.get() 35 | 36 | init { 37 | // Simulate initialization in background thread 38 | Thread { 39 | try { 40 | Thread.sleep(100) // Simulate initialization time 41 | isInitialized.set(true) 42 | initializationLatch.countDown() 43 | } catch (e: InterruptedException) { 44 | Thread.currentThread().interrupt() 45 | } 46 | }.start() 47 | } 48 | 49 | fun getComponent(): JComponent { 50 | // Return the same component instance to maintain consistency 51 | return mockComponent 52 | } 53 | 54 | // Add method to check component validity for tests 55 | fun isComponentValid(): Boolean { 56 | return componentValid.get() && !_isDisposed.get() 57 | } 58 | 59 | fun postMessage(message: String) { 60 | if (_isDisposed.get()) { 61 | throw IllegalStateException("ChatWebView is disposed") 62 | } 63 | 64 | // Simulate string message posting 65 | // For testing, we just validate it's not empty 66 | if (message.isBlank()) { 67 | throw IllegalArgumentException("Message cannot be blank") 68 | } 69 | messageCount.incrementAndGet() 70 | } 71 | 72 | fun setStyle() { 73 | if (_isDisposed.get()) { 74 | throw IllegalStateException("ChatWebView is disposed") 75 | } 76 | 77 | // Simulate style setting 78 | // In real implementation, this would update browser theme 79 | styleUpdateCount.incrementAndGet() 80 | } 81 | 82 | // Test utility methods 83 | fun waitForInitialization(timeoutMs: Long = 5000): Boolean { 84 | return try { 85 | initializationLatch.await(timeoutMs, TimeUnit.MILLISECONDS) 86 | } catch (e: InterruptedException) { 87 | Thread.currentThread().interrupt() 88 | false 89 | } 90 | } 91 | 92 | fun waitForDisposal(timeoutMs: Long = 5000): Boolean { 93 | return try { 94 | disposalLatch.await(timeoutMs, TimeUnit.MILLISECONDS) 95 | } catch (e: InterruptedException) { 96 | Thread.currentThread().interrupt() 97 | false 98 | } 99 | } 100 | 101 | // Simulate receiving a message from browser (for testing) 102 | fun simulateMessageFromBrowser(message: String) { 103 | if (_isDisposed.get()) return 104 | 105 | try { 106 | val event = Events.parse(message) 107 | if (event != null) { 108 | messageHandler(event) 109 | } 110 | } catch (e: Exception) { 111 | // Ignore parsing errors in tests 112 | } 113 | } 114 | 115 | override fun dispose() { 116 | if (_isDisposed.compareAndSet(false, true)) { 117 | // Mark component as invalid 118 | componentValid.set(false) 119 | 120 | // Simulate disposal cleanup 121 | Thread { 122 | try { 123 | Thread.sleep(50) // Simulate cleanup time 124 | disposalLatch.countDown() 125 | } catch (e: InterruptedException) { 126 | Thread.currentThread().interrupt() 127 | disposalLatch.countDown() 128 | } 129 | }.start() 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/kotlin/com/smallcloud/refactai/modes/diff/renderer/PanelRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.modes.diff.renderer 2 | 3 | import com.intellij.openapi.Disposable 4 | import com.intellij.openapi.application.ApplicationManager 5 | import com.intellij.openapi.editor.Editor 6 | import com.intellij.openapi.editor.EditorCustomElementRenderer 7 | import com.intellij.openapi.editor.Inlay 8 | import com.intellij.openapi.editor.event.EditorMouseEvent 9 | import com.intellij.openapi.editor.event.EditorMouseListener 10 | import com.intellij.openapi.editor.event.EditorMouseMotionListener 11 | import com.intellij.openapi.editor.markup.TextAttributes 12 | import com.intellij.util.ui.UIUtil 13 | import java.awt.Cursor 14 | import java.awt.Graphics 15 | import java.awt.Point 16 | import java.awt.Rectangle 17 | import java.awt.event.MouseEvent 18 | 19 | 20 | enum class Style { 21 | Normal, Underlined 22 | } 23 | 24 | class PanelRenderer( 25 | private val firstSymbolPos: Point, 26 | private val editor: Editor, 27 | private val labels: List Unit>> 28 | ) : EditorCustomElementRenderer, EditorMouseListener, EditorMouseMotionListener, Disposable { 29 | private var inlayVisitor: Inlay<*>? = null 30 | private var xBounds: MutableList> = mutableListOf() 31 | private val styles: MutableList