├── .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 |
5 |
6 |
7 |
8 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
3 |
4 |
5 | ---
6 |
7 | [](https://smallcloud.ai/discord)
8 | [](https://twitter.com/intent/follow?screen_name=refact_ai)
9 | 
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 |
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