├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .idea ├── .gitignore ├── .name ├── gradle.xml ├── kotlinc.xml └── vcs.xml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── almost-all-features-05x.jpg ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── refact_lsp ├── settings.gradle.kts └── src ├── main ├── kotlin │ └── com │ │ └── smallcloud │ │ └── refactai │ │ ├── FimCache.kt │ │ ├── Initializer.kt │ │ ├── PluginErrorReportSubmitter.kt │ │ ├── PluginState.kt │ │ ├── RefactAIBundle.kt │ │ ├── Resources.kt │ │ ├── UpdateChecker.kt │ │ ├── account │ │ ├── AccountManager.kt │ │ └── AccountManagerChangedNotifier.kt │ │ ├── code_lens │ │ ├── CodeLensAction.kt │ │ ├── CodeLensInvalidatorService.kt │ │ ├── RefactCodeVisionProvider.kt │ │ └── RefactCodeVisionProviderFactory.kt │ │ ├── codecompletion │ │ ├── InlineCompletionGrayTextElement.kt │ │ ├── RefactAICompletionProvider.kt │ │ ├── RefactAIContinuousEvent.kt │ │ └── RefactInlineCompletionDocumentListener.kt │ │ ├── io │ │ ├── AsyncConnection.kt │ │ ├── CloudMessageService.kt │ │ ├── Connection.kt │ │ ├── Fetch.kt │ │ ├── InferenceGlobalContext.kt │ │ ├── InferenceGlobalContextChangedNotifier.kt │ │ └── RequestHelpers.kt │ │ ├── listeners │ │ ├── AcceptAction.kt │ │ ├── AcceptActionPromoter.kt │ │ ├── CancelAction.kt │ │ ├── CancelActionPromoter.kt │ │ ├── DocumentListener.kt │ │ ├── ForceCompletionAction.kt │ │ ├── ForceCompletionActionPromoter.kt │ │ ├── GenerateGitCommitMessageAction.kt │ │ ├── GlobalCaretListener.kt │ │ ├── GlobalFocusListener.kt │ │ ├── InlineActionPromoter.kt │ │ ├── LSPDocumentListener.kt │ │ ├── LastEditorGetterListener.kt │ │ ├── PluginListener.kt │ │ └── UninstallListener.kt │ │ ├── lsp │ │ ├── LSPActiveDocNotifierService.kt │ │ ├── LSPCapabilities.kt │ │ ├── LSPConfig.kt │ │ ├── LSPHelper.kt │ │ ├── LSPProcessHolder.kt │ │ ├── LSPTools.kt │ │ └── RagStatus.kt │ │ ├── modes │ │ ├── EditorTextState.kt │ │ ├── EventAdapter.kt │ │ ├── Mode.kt │ │ ├── ModeProvider.kt │ │ ├── completion │ │ │ ├── CompletionTracker.kt │ │ │ ├── StubCompletionMode.kt │ │ │ ├── prompt │ │ │ │ └── RequestCreator.kt │ │ │ └── structs │ │ │ │ ├── Completion.kt │ │ │ │ └── DocumentEventExtra.kt │ │ └── diff │ │ │ ├── DiffLayout.kt │ │ │ ├── DiffMode.kt │ │ │ ├── Utils.kt │ │ │ ├── renderer │ │ │ ├── BlockRenderer.kt │ │ │ ├── Inlayer.kt │ │ │ ├── PanelRenderer.kt │ │ │ └── RenderHelper.kt │ │ │ └── waitingDiff.kt │ │ ├── notifications │ │ ├── Initializer.kt │ │ └── Notifications.kt │ │ ├── panes │ │ ├── RefactAIToolboxPaneFactory.kt │ │ └── sharedchat │ │ │ ├── ChatPaneInvokeAction.kt │ │ │ ├── ChatPaneInvokeActionPromoter.kt │ │ │ ├── ChatPanes.kt │ │ │ ├── Editor.kt │ │ │ ├── Events.kt │ │ │ ├── SharedChatPane.kt │ │ │ └── browser │ │ │ ├── ChatWebView.kt │ │ │ └── RequestHandler.kt │ │ ├── settings │ │ ├── AppSettingsComponent.kt │ │ ├── AppSettingsConfigurable.kt │ │ ├── AppSettingsState.kt │ │ └── Host.kt │ │ ├── statistic │ │ ├── UsageStatistic.kt │ │ └── UsageStats.kt │ │ ├── status_bar │ │ ├── StatusBarComponent.kt │ │ ├── StatusBarProvider.kt │ │ └── StatusBarWidget.kt │ │ ├── struct │ │ ├── ChatMessage.kt │ │ ├── DeploymentMode.kt │ │ ├── Exceptions.kt │ │ ├── SMCPrediction.kt │ │ └── SMCRequest.kt │ │ └── utils │ │ ├── JCefUtils.kt │ │ ├── LastProjectGetter.kt │ │ ├── LinksPanel.kt │ │ └── getExtension.kt └── resources │ ├── META-INF │ ├── plugin.xml │ └── pluginIcon.svg │ ├── bundles │ └── RefactAI.properties │ ├── icons │ ├── coin_16x16.svg │ ├── hand_12x12.svg │ ├── refact_logo.svg │ ├── refactai_logo_12x12.svg │ ├── refactai_logo_red_12x12.svg │ ├── refactai_logo_red_13x13.svg │ └── refactai_logo_red_16x16.svg │ └── webview │ └── index.html └── test └── kotlin └── com └── smallcloud └── refactai ├── io └── AsyncConnectionTest.kt ├── lsp ├── LSPProcessHolderRaceTest.kt ├── LSPProcessHolderTest.kt ├── LSPProcessHolderTimeoutTest.kt └── LspToolsTest.kt ├── panes └── sharedchat │ ├── ChatWebViewTest.kt │ └── EventsTest.kt └── testUtils └── MockServer.kt /.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 -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | refact -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Refact 3 |

4 | 5 | --- 6 | 7 | [![Discord](https://img.shields.io/discord/1037660742440194089?logo=discord&label=Discord&link=https%3A%2F%2Fsmallcloud.ai%2Fdiscord)](https://smallcloud.ai/discord) 8 | [![Twitter Follow](https://img.shields.io/twitter/follow/refact_ai)](https://twitter.com/intent/follow?screen_name=refact_ai) 9 | ![License](https://img.shields.io/github/license/smallcloudai/refact-intellij) 10 | 11 | 12 | # Refact-intellij 13 | *Refact for JetBrains is a free, open-source AI code assistant* 14 | 15 | ## Features 16 | 17 | - Access to 20+ LLMs: Leverage powerful language models, including GPT-4, Refact/1.6B, Code Llama, StarCoder2, Mistral, Mixtral, and more. Some models offer the ability to fine-tune for specialized needs. 18 | 19 | - CodeLens Integration: Get detailed insights into your code directly from your IDE using CodeLens. This feature enhances code navigation and understanding- CodeLens Integration: As you write code, you can instantly call a chat to find bugs or ask for an explanation of any code snippet. 20 | 21 | - FIM Debug Page: Access debugging tools and insights through the FIM debug page for improved troubleshooting. 22 | 23 | - Upload Images within Your IDE : Save time with our new in-IDE image upload feature—seamlessly integrated for your convenience. Easily upload one or multiple images to streamline your workflow 24 | 25 | 26 | ## Getting Started 27 | Once [installed](https://plugins.jetbrains.com/plugin/20647-refact-ai), look for the Refact.ai logo in the status bar or the sidebar, click 'login', and agree to T&C. Start typing some code, and autocomplete will make suggestions automatically! Press F1 to access the AI toolbox functions. Refact has a simple, user-friendly interface that makes it easy to use, even for those new to AI tools. 28 | 29 | If you have your own NVIDIA GPU, you can try the [self-hosted version](https://github.com/smallcloudai/refact). 30 | ## Support & Feedback 31 | Join our Discord to get to know other community members, send us feedback or suggestions, and get support. 32 | -------------------------------------------------------------------------------- /almost-all-features-05x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smallcloudai/refact-intellij/5fa5b9048e9ce6d81134c51fe0689c3b6f0fa2d4/almost-all-features-05x.jpg -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType 2 | import org.jetbrains.intellij.platform.gradle.TestFrameworkType 3 | import org.jetbrains.intellij.platform.gradle.tasks.VerifyPluginTask 4 | 5 | plugins { 6 | id("java") // Java support 7 | alias(libs.plugins.kotlin) // Kotlin support 8 | alias(libs.plugins.intelliJPlatform) // IntelliJ Platform Gradle Plugin 9 | alias(libs.plugins.changelog) // Gradle Changelog Plugin 10 | alias(libs.plugins.qodana) // Gradle Qodana Plugin 11 | alias(libs.plugins.kover) // Gradle Kover Plugin 12 | } 13 | 14 | group = providers.gradleProperty("pluginGroup").get() 15 | version = getVersionString(providers.gradleProperty("pluginVersion").get()) 16 | 17 | val javaCompilerVersion = "17" 18 | kotlin { 19 | jvmToolchain(javaCompilerVersion.toInt()) 20 | } 21 | 22 | repositories { 23 | mavenCentral() 24 | 25 | intellijPlatform { 26 | defaultRepositories() 27 | } 28 | } 29 | 30 | dependencies { 31 | implementation("dev.gitlive:kotlin-diff-utils:5.0.7") 32 | implementation("org.apache.httpcomponents.client5:httpclient5:5.3.1") { 33 | exclude("org.slf4j") 34 | } 35 | implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.10") 36 | implementation("com.vladsch.flexmark:flexmark-all:0.64.8") 37 | implementation("io.github.kezhenxu94:cache-lite:0.2.0") 38 | 39 | // test libraries 40 | testImplementation(kotlin("test")) 41 | testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0") 42 | testImplementation("org.bouncycastle:bcpkix-jdk15on:1.68") 43 | testImplementation("org.mockito:mockito-core:5.10.0") 44 | testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1") 45 | 46 | intellijPlatform { 47 | create(providers.gradleProperty("platformType"), providers.gradleProperty("platformVersion")) 48 | 49 | // Plugin Dependencies. Uses `platformBundledPlugins` property from the gradle.properties file for bundled IntelliJ Platform plugins. 50 | bundledPlugins(providers.gradleProperty("platformBundledPlugins").map { it.split(',') }) 51 | 52 | // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file for plugin from JetBrains Marketplace. 53 | plugins(providers.gradleProperty("platformPlugins").map { it.split(',') }) 54 | 55 | instrumentationTools() 56 | pluginVerifier() 57 | zipSigner() 58 | testFramework(TestFrameworkType.Platform) 59 | } 60 | } 61 | 62 | intellijPlatform { 63 | pluginConfiguration { 64 | ideaVersion { 65 | sinceBuild = providers.gradleProperty("pluginSinceBuild") 66 | untilBuild = providers.gradleProperty("pluginUntilBuild") 67 | } 68 | } 69 | 70 | signing { 71 | certificateChain = providers.environmentVariable("CERTIFICATE_CHAIN") 72 | privateKey = providers.environmentVariable("PRIVATE_KEY") 73 | password = providers.environmentVariable("PRIVATE_KEY_PASSWORD") 74 | } 75 | 76 | publishing { 77 | token = providers.environmentVariable("PUBLISH_TOKEN") 78 | channels = providers.environmentVariable("PUBLISH_CHANNEL").map { listOf(it) } 79 | } 80 | 81 | pluginVerification { 82 | failureLevel = listOf( 83 | VerifyPluginTask.FailureLevel.INTERNAL_API_USAGES, 84 | VerifyPluginTask.FailureLevel.COMPATIBILITY_PROBLEMS, 85 | VerifyPluginTask.FailureLevel.INVALID_PLUGIN, 86 | ) 87 | ides { 88 | recommended() 89 | } 90 | } 91 | } 92 | 93 | val runIdeWith2025 by intellijPlatformTesting.runIde.registering { 94 | type = IntelliJPlatformType.PyCharmCommunity // or IdeaUltimate if you use IU 95 | version = "2025.1" 96 | useInstaller = false 97 | } 98 | 99 | tasks { 100 | // Set the JVM compatibility versions 101 | withType { 102 | sourceCompatibility = javaCompilerVersion 103 | targetCompatibility = javaCompilerVersion 104 | } 105 | withType { 106 | kotlinOptions.jvmTarget = javaCompilerVersion 107 | } 108 | } 109 | 110 | fun runCommand(cmd: String): String { 111 | return providers.exec { 112 | commandLine(cmd.split(" ")) 113 | }.standardOutput.asText.get().trim() 114 | } 115 | 116 | fun getVersionString(baseVersion: String): String { 117 | val tag = runCommand("git tag -l --points-at HEAD") 118 | 119 | if (System.getenv("PUBLISH_EAP") != "1" && 120 | tag.isNotEmpty() && tag.contains(baseVersion)) return baseVersion 121 | 122 | val branch = runCommand("git rev-parse --abbrev-ref HEAD").replace("/", "-") 123 | val numberOfCommits = if (branch == "main") { 124 | val lastTag = runCommand("git describe --tags --abbrev=0 @^") 125 | runCommand("git rev-list ${lastTag}..HEAD --count") 126 | } else { 127 | runCommand("git rev-list --count HEAD ^origin/main") 128 | } 129 | val commitId = runCommand("git rev-parse --short=8 HEAD") 130 | return if (System.getenv("PUBLISH_EAP") == "1") { 131 | "$baseVersion.$numberOfCommits-eap-$commitId" 132 | } else { 133 | "$baseVersion-$branch-$numberOfCommits-$commitId" 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /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.4.1 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 = 253.* 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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smallcloudai/refact-intellij/5fa5b9048e9ce6d81134c51fe0689c3b6f0fa2d4/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /refact_lsp: -------------------------------------------------------------------------------- 1 | dev -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "refact" -------------------------------------------------------------------------------- /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/Initializer.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai 2 | 3 | import com.intellij.ide.plugins.PluginInstaller 4 | import com.intellij.openapi.Disposable 5 | import com.intellij.openapi.application.ApplicationManager 6 | import com.intellij.openapi.application.invokeLater 7 | import com.intellij.openapi.diagnostic.Logger 8 | import com.intellij.openapi.project.Project 9 | import com.intellij.openapi.startup.ProjectActivity 10 | import com.smallcloud.refactai.io.CloudMessageService 11 | import com.smallcloud.refactai.listeners.UninstallListener 12 | import com.smallcloud.refactai.lsp.LSPActiveDocNotifierService 13 | import com.smallcloud.refactai.lsp.LSPProcessHolder.Companion.initialize 14 | import com.smallcloud.refactai.notifications.emitInfo 15 | import com.smallcloud.refactai.notifications.notificationStartup 16 | import com.smallcloud.refactai.panes.sharedchat.ChatPaneInvokeAction 17 | import com.smallcloud.refactai.settings.AppSettingsState 18 | import com.smallcloud.refactai.settings.settingsStartup 19 | import com.smallcloud.refactai.utils.isJcefCanStart 20 | import java.util.concurrent.atomic.AtomicBoolean 21 | import com.smallcloud.refactai.lsp.LSPProcessHolder.Companion.getInstance as getLSPProcessHolder 22 | 23 | class Initializer : ProjectActivity, Disposable { 24 | override suspend fun execute(project: Project) { 25 | val shouldInitialize = !(initialized.getAndSet(true) || ApplicationManager.getApplication().isUnitTestMode) 26 | if (shouldInitialize) { 27 | Logger.getInstance("SMCInitializer").info("Bin prefix = ${Resources.binPrefix}") 28 | initialize() 29 | if (AppSettingsState.instance.isFirstStart) { 30 | AppSettingsState.instance.isFirstStart = false 31 | invokeLater { ChatPaneInvokeAction().actionPerformed() } 32 | } 33 | settingsStartup() 34 | notificationStartup() 35 | PluginInstaller.addStateListener(UninstallListener()) 36 | UpdateChecker.instance 37 | 38 | ApplicationManager.getApplication().getService(CloudMessageService::class.java) 39 | if (!isJcefCanStart()) { 40 | emitInfo(RefactAIBundle.message("notifications.chatCanNotStartWarning"), false) 41 | } 42 | } 43 | getLSPProcessHolder(project) 44 | project.getService(LSPActiveDocNotifierService::class.java) 45 | } 46 | 47 | override fun dispose() { 48 | } 49 | 50 | } 51 | 52 | private val initialized = AtomicBoolean(false) -------------------------------------------------------------------------------- /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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/code_lens/RefactCodeVisionProvider.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.code_lens 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.JsonObject 5 | import com.intellij.codeInsight.codeVision.* 6 | import com.intellij.codeInsight.codeVision.ui.model.ClickableTextCodeVisionEntry 7 | import com.intellij.openapi.application.runReadAction 8 | import com.intellij.openapi.diagnostic.Logger 9 | import com.intellij.openapi.editor.Editor 10 | import com.intellij.openapi.editor.LogicalPosition 11 | import com.intellij.openapi.util.TextRange 12 | import com.smallcloud.refactai.Resources 13 | import com.smallcloud.refactai.lsp.LSPProcessHolder.Companion.getInstance 14 | import com.smallcloud.refactai.lsp.lspGetCodeLens 15 | import com.smallcloud.refactai.struct.ChatMessage 16 | import kotlin.math.max 17 | import com.intellij.codeInsight.codeVision.CodeVisionBundle 18 | data class CodeLen( 19 | val range: TextRange, 20 | val label: String, 21 | val action: CodeLensAction 22 | ) 23 | 24 | fun makeIdForProvider(commandKey: String): String { 25 | return "refactai.codelens.$commandKey" 26 | } 27 | 28 | class RefactCodeVisionProvider( 29 | private val commandKey: String, 30 | private val posAfter: String?, 31 | private val label: String, 32 | private val customization: JsonObject 33 | ) : 34 | CodeVisionProvider { 35 | override val defaultAnchor: CodeVisionAnchorKind 36 | get() = CodeVisionAnchorKind.Top 37 | override val id: String 38 | get() = makeIdForProvider(commandKey) 39 | override val name: String 40 | get() = "Refact.ai Hint($label)" 41 | override val relativeOrderings: List 42 | get() { 43 | return if (posAfter == null) { 44 | listOf(CodeVisionRelativeOrdering.CodeVisionRelativeOrderingFirst) 45 | } else { 46 | listOf(CodeVisionRelativeOrdering.CodeVisionRelativeOrderingAfter("refactai.codelens.$posAfter")) 47 | } 48 | } 49 | 50 | override fun precomputeOnUiThread(editor: Editor) {} 51 | 52 | private fun getCodeLens(editor: Editor): List { 53 | val codeLensStr = lspGetCodeLens(editor) 54 | val gson = Gson() 55 | val codeLensJson = gson.fromJson(codeLensStr, JsonObject::class.java) 56 | val resCodeLenses = mutableListOf() 57 | if (customization.has("code_lens")) { 58 | val allCodeLenses = customization.get("code_lens").asJsonObject 59 | if (codeLensJson.has("code_lens")) { 60 | val codeLenses = codeLensJson.get("code_lens")!!.asJsonArray 61 | for (codeLens in codeLenses) { 62 | val line1 = max(codeLens.asJsonObject.get("line1").asInt - 1, 0) 63 | val line2 = max(codeLens.asJsonObject.get("line2").asInt - 1, 0) 64 | val range = runReadAction { 65 | return@runReadAction TextRange( 66 | editor.logicalPositionToOffset(LogicalPosition(line1, 0)), 67 | editor.document.getLineEndOffset(line2) 68 | ) 69 | } 70 | val value = allCodeLenses.get(commandKey).asJsonObject 71 | val msgs = value.asJsonObject.get("messages").asJsonArray.map { 72 | gson.fromJson(it.asJsonObject, ChatMessage::class.java) 73 | }.toTypedArray() 74 | val userMsg = msgs.find { it.role == "user" } 75 | 76 | val sendImmediately = value.asJsonObject.get("auto_submit").asBoolean 77 | val openNewTab = value.asJsonObject.get("new_tab")?.asBoolean ?: true 78 | 79 | val isValidCodeLen = msgs.isEmpty() || userMsg != null 80 | if (isValidCodeLen) { 81 | resCodeLenses.add( 82 | CodeLen( 83 | range, 84 | value.asJsonObject.get("label").asString, 85 | CodeLensAction(editor, line1, line2, msgs, sendImmediately, openNewTab) 86 | ) 87 | ) 88 | } 89 | } 90 | } 91 | } 92 | 93 | return resCodeLenses 94 | } 95 | 96 | override fun computeCodeVision(editor: Editor, uiData: Unit): CodeVisionState { 97 | // Logger.getInstance(RefactCodeVisionProvider::class.java).warn("computeCodeVision $commandKey start") 98 | val lsp = editor.project?.let { getInstance(it) } ?: return CodeVisionState.NotReady 99 | if (!lsp.isWorking) return CodeVisionState.NotReady 100 | 101 | try { 102 | val codeLens = getCodeLens(editor) 103 | val result = ArrayList>() 104 | // Logger.getInstance(RefactCodeVisionProvider::class.java) 105 | // .warn("computeCodeVision $commandKey ${codeLens.size}") 106 | for (codeLen in codeLens) { 107 | result.add(codeLen.range to ClickableTextCodeVisionEntry(codeLen.label, id, { _, _ -> 108 | codeLen.action.actionPerformed() 109 | }, Resources.Icons.LOGO_12x12)) 110 | } 111 | return CodeVisionState.Ready(result) 112 | } catch (e: Exception) { 113 | return CodeVisionState.NotReady 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/listeners/GenerateGitCommitMessageAction.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.listeners 2 | 3 | import com.intellij.openapi.actionSystem.ActionUpdateThread 4 | import com.intellij.openapi.actionSystem.AnAction 5 | import com.intellij.openapi.actionSystem.AnActionEvent 6 | import com.intellij.openapi.application.ApplicationManager 7 | import com.intellij.openapi.components.service 8 | import com.intellij.openapi.diff.impl.patch.IdeaTextPatchBuilder 9 | import com.intellij.openapi.diff.impl.patch.UnifiedDiffWriter 10 | import com.intellij.openapi.project.Project 11 | import com.intellij.openapi.vcs.VcsDataKeys 12 | import com.intellij.openapi.vcs.VcsException 13 | import com.intellij.openapi.vcs.changes.Change 14 | import com.intellij.openapi.vcs.changes.CurrentContentRevision 15 | import com.intellij.ui.AnimatedIcon 16 | import com.intellij.vcsUtil.VcsUtil 17 | import com.smallcloud.refactai.RefactAIBundle 18 | import com.smallcloud.refactai.Resources 19 | import com.smallcloud.refactai.lsp.lspGetCommitMessage 20 | import java.io.IOException 21 | import java.io.StringWriter 22 | import java.util.concurrent.ExecutionException 23 | 24 | 25 | class GenerateGitCommitMessageAction : AnAction( 26 | Resources.titleStr, 27 | RefactAIBundle.message("generateCommitMessage.action.description"), 28 | Resources.Icons.LOGO_RED_13x13 29 | ) { 30 | private val spinIcon = AnimatedIcon.Default.INSTANCE 31 | override fun update(event: AnActionEvent) { 32 | ApplicationManager.getApplication().invokeLater { 33 | val commitWorkflowUi = event.getData(VcsDataKeys.COMMIT_WORKFLOW_UI) 34 | if (commitWorkflowUi == null) { 35 | event.presentation.isVisible = false 36 | return@invokeLater 37 | } 38 | 39 | val lspService = 40 | event.project?.service() ?: return@invokeLater 41 | 42 | val isEnabled = lspService.isWorking && (commitWorkflowUi.getIncludedChanges().isNotEmpty() || commitWorkflowUi.getIncludedUnversionedFiles().isNotEmpty()) 43 | 44 | event.presentation.isEnabled = isEnabled 45 | event.presentation.text = if (lspService.isWorking) { 46 | RefactAIBundle.message("generateCommitMessage.action.selectFiles") 47 | } else { 48 | RefactAIBundle.message("generateCommitMessage.action.loginInRefactAI") 49 | } 50 | } 51 | } 52 | 53 | override fun actionPerformed(event: AnActionEvent) { 54 | val project = event.project 55 | if (project == null || project.basePath == null) { 56 | return 57 | } 58 | 59 | val gitDiff = getDiff(event, project) ?: return 60 | val commitWorkflowUi = event.getData(VcsDataKeys.COMMIT_WORKFLOW_UI) 61 | if (commitWorkflowUi != null) { 62 | ApplicationManager.getApplication().executeOnPooledThread { 63 | event.presentation.icon = spinIcon 64 | ApplicationManager.getApplication().invokeLater { 65 | commitWorkflowUi.commitMessageUi.stopLoading() 66 | } 67 | val message = lspGetCommitMessage(project, gitDiff, commitWorkflowUi.commitMessageUi.text) 68 | ApplicationManager.getApplication().invokeLater { 69 | commitWorkflowUi.commitMessageUi.stopLoading() 70 | commitWorkflowUi.commitMessageUi.setText(message) 71 | } 72 | event.presentation.icon = Resources.Icons.LOGO_RED_13x13 73 | } 74 | } 75 | } 76 | 77 | override fun getActionUpdateThread(): ActionUpdateThread { 78 | return ActionUpdateThread.EDT 79 | } 80 | 81 | private fun getDiff(event: AnActionEvent, project: Project): String? { 82 | val commitWorkflowUi = event.getData(VcsDataKeys.COMMIT_WORKFLOW_UI) 83 | ?: throw IllegalStateException("Could not retrieve commit workflow ui.") 84 | 85 | try { 86 | val projectFile = project.projectFile ?: return null 87 | val projectFileVcsRoot = VcsUtil.getVcsRootFor(project, projectFile) ?: return null 88 | 89 | try { 90 | val includedChanges = commitWorkflowUi.getIncludedChanges().toMutableList() 91 | val includedUnversionedFiles = commitWorkflowUi.getIncludedUnversionedFiles() 92 | if (!includedUnversionedFiles.isEmpty()) { 93 | for (filePath in includedUnversionedFiles) { 94 | val change: Change = Change(null, CurrentContentRevision(filePath)) 95 | includedChanges.add(change) 96 | } 97 | } 98 | val filePatches = IdeaTextPatchBuilder.buildPatch( 99 | project, includedChanges, projectFileVcsRoot.toNioPath(), false, true 100 | ) 101 | 102 | val diffWriter = StringWriter() 103 | UnifiedDiffWriter.write( 104 | null, 105 | projectFileVcsRoot.toNioPath(), 106 | filePatches, 107 | diffWriter, 108 | "\n", 109 | null, 110 | null 111 | ) 112 | return diffWriter.toString() 113 | } catch (e: VcsException) { 114 | throw RuntimeException("Unable to create git diff", e) 115 | } catch (e: IOException) { 116 | throw RuntimeException("Unable to create git diff", e) 117 | } 118 | } catch (e: InterruptedException) { 119 | throw RuntimeException(e) 120 | } catch (e: ExecutionException) { 121 | throw RuntimeException(e) 122 | } 123 | } 124 | } -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/modes/ModeProvider.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.modes 2 | 3 | import com.intellij.openapi.editor.Editor 4 | import com.intellij.codeInsight.completion.CompletionUtil.DUMMY_IDENTIFIER 5 | import com.intellij.openapi.Disposable 6 | import com.intellij.openapi.actionSystem.DataContext 7 | import com.intellij.openapi.application.ApplicationManager 8 | import com.intellij.openapi.editor.Caret 9 | import com.intellij.openapi.editor.event.CaretEvent 10 | import com.intellij.openapi.editor.event.DocumentEvent 11 | import com.intellij.openapi.editor.ex.EditorEx 12 | import com.intellij.util.ObjectUtils 13 | import com.intellij.util.concurrency.AppExecutorUtil 14 | import com.intellij.util.messages.MessageBus 15 | import com.intellij.util.xmlb.annotations.Transient 16 | import com.jetbrains.rd.util.getOrCreate 17 | import com.smallcloud.refactai.io.ConnectionStatus 18 | import com.smallcloud.refactai.io.InferenceGlobalContextChangedNotifier 19 | import com.smallcloud.refactai.listeners.GlobalCaretListener 20 | import com.smallcloud.refactai.listeners.GlobalFocusListener 21 | import com.smallcloud.refactai.modes.completion.StubCompletionMode 22 | import com.smallcloud.refactai.modes.completion.structs.DocumentEventExtra 23 | import com.smallcloud.refactai.modes.diff.DiffMode 24 | import com.smallcloud.refactai.modes.diff.DiffModeWithSideEffects 25 | import com.smallcloud.refactai.statistic.UsageStatistic 26 | import com.smallcloud.refactai.statistic.UsageStats 27 | import java.lang.System.currentTimeMillis 28 | import java.lang.System.identityHashCode 29 | import java.util.concurrent.ConcurrentLinkedQueue 30 | import java.util.concurrent.TimeUnit 31 | import com.smallcloud.refactai.io.InferenceGlobalContext.Companion.instance as InferenceGlobalContext 32 | 33 | 34 | enum class ModeType { 35 | Completion, 36 | Diff, 37 | DiffWithSideEffects, 38 | } 39 | 40 | class ModeProvider( 41 | private val editor: Editor, 42 | private val modes: MutableMap = mutableMapOf( 43 | ModeType.Completion to StubCompletionMode(), 44 | ModeType.Diff to DiffMode() 45 | ), 46 | private var activeMode: Mode? = null, 47 | ) : Disposable, InferenceGlobalContextChangedNotifier { 48 | 49 | @Transient 50 | private val messageBus: MessageBus = ApplicationManager.getApplication().messageBus 51 | 52 | init { 53 | activeMode = modes[ModeType.Completion] 54 | messageBus.connect(this).subscribe( 55 | InferenceGlobalContextChangedNotifier.TOPIC, this 56 | ) 57 | } 58 | 59 | fun modeInActiveState(): Boolean = activeMode?.isInActiveState() == true 60 | 61 | fun isInCompletionMode(): Boolean = 62 | activeMode === modes[ModeType.Completion] 63 | fun isDiffMode(): Boolean = activeMode == modes[ModeType.Diff] || activeMode == modes[ModeType.DiffWithSideEffects] 64 | fun getCompletionMode(): Mode = modes[ModeType.Completion]!! 65 | 66 | fun beforeDocumentChangeNonBulk(event: DocumentEvent?, editor: Editor) { 67 | if (event?.newFragment.toString() == DUMMY_IDENTIFIER) return 68 | activeMode?.beforeDocumentChangeNonBulk(DocumentEventExtra(event, editor, currentTimeMillis())) 69 | } 70 | 71 | fun onTextChange(event: DocumentEvent?, editor: Editor, force: Boolean) { 72 | if (event?.newFragment.toString() == DUMMY_IDENTIFIER) return 73 | activeMode?.onTextChange(DocumentEventExtra(event, editor, currentTimeMillis(), force)) 74 | } 75 | 76 | fun onCaretChange(event: CaretEvent) { 77 | activeMode?.onCaretChange(event) 78 | } 79 | 80 | fun focusGained() {} 81 | 82 | fun focusLost() {} 83 | 84 | fun onTabPressed(editor: Editor, caret: Caret?, dataContext: DataContext) { 85 | activeMode?.onTabPressed(editor, caret, dataContext) 86 | } 87 | 88 | fun onEscPressed(editor: Editor, caret: Caret?, dataContext: DataContext) { 89 | activeMode?.onEscPressed(editor, caret, dataContext) 90 | } 91 | 92 | override fun dispose() { 93 | } 94 | 95 | fun switchMode(newMode: ModeType = ModeType.Completion) { 96 | if (activeMode == modes[newMode]) return 97 | activeMode?.cleanup(editor) 98 | activeMode = modes[newMode] 99 | } 100 | 101 | fun removeSideEffects() { 102 | activeMode?.cleanup(editor) 103 | modes.remove(ModeType.DiffWithSideEffects) 104 | activeMode = null 105 | } 106 | 107 | fun addSideEffects(onTab: (Editor, Caret?, DataContext) -> Unit, onEsc: (Editor, Caret?, DataContext) -> Unit): DiffModeWithSideEffects { 108 | fun handleFn(fn: T): T { 109 | removeSideEffects() 110 | return fn 111 | } 112 | val mode = DiffModeWithSideEffects(handleFn(onTab), handleFn(onEsc)) 113 | modes.set(ModeType.DiffWithSideEffects, mode) 114 | this.switchMode(ModeType.DiffWithSideEffects) 115 | return mode 116 | } 117 | 118 | 119 | 120 | fun getDiffMode(): DiffMode = (modes[ModeType.Diff] as DiffMode?)!! 121 | 122 | companion object { 123 | private const val MAX_EDITORS: Int = 8 124 | private var modeProviders: LinkedHashMap = linkedMapOf() 125 | private var providersToTs: LinkedHashMap = linkedMapOf() 126 | 127 | fun getOrCreateModeProvider(editor: Editor): ModeProvider { 128 | val hashId = identityHashCode(editor) 129 | if (modeProviders.size > MAX_EDITORS) { 130 | val toRemove = providersToTs.minByOrNull { it.value }?.key 131 | providersToTs.remove(toRemove) 132 | modeProviders.remove(toRemove) 133 | } 134 | return modeProviders.getOrCreate(hashId) { 135 | val modeProvider = ModeProvider(editor) 136 | providersToTs[hashId] = currentTimeMillis() 137 | editor.caretModel.addCaretListener(GlobalCaretListener()) 138 | ObjectUtils.consumeIfCast(editor, EditorEx::class.java) { 139 | try { 140 | it.addFocusListener(GlobalFocusListener(), modeProvider) 141 | } catch (e: UnsupportedOperationException) { 142 | // nothing 143 | } 144 | } 145 | modeProvider 146 | } 147 | } 148 | } 149 | } 150 | 151 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 27 | 28 | 29 |
30 | 31 | -------------------------------------------------------------------------------- /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/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/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/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/test/kotlin/com/smallcloud/refactai/panes/sharedchat/ChatWebViewTest.kt: -------------------------------------------------------------------------------- 1 | package com.smallcloud.refactai.panes.sharedchat 2 | 3 | import com.intellij.ide.ui.LafManager 4 | import com.intellij.ide.ui.laf.UIThemeLookAndFeelInfo 5 | import com.intellij.openapi.diagnostic.Logger 6 | import com.intellij.openapi.project.Project 7 | import com.intellij.testFramework.LightPlatform4TestCase 8 | import com.smallcloud.refactai.panes.sharedchat.browser.ChatWebView 9 | import org.junit.Test 10 | import org.junit.Assert 11 | import org.junit.Ignore 12 | import org.mockito.Mockito 13 | import org.mockito.MockedStatic 14 | 15 | /** 16 | * Test for ChatWebView to verify it handles race conditions properly. 17 | * This test specifically checks that the ChatWebView can handle a situation where 18 | * setStyle() is called before the browser is fully initialized, and then the 19 | * component is disposed while JavaScript might still be executing. 20 | */ 21 | class ChatWebViewTest: LightPlatform4TestCase() { 22 | private lateinit var mockProject: Project 23 | private lateinit var mockEditor: Editor 24 | private lateinit var mockLafManager: LafManager 25 | private lateinit var mockTheme: UIThemeLookAndFeelInfo 26 | private lateinit var mockLafManagerStatic: MockedStatic 27 | 28 | 29 | override fun setUp() { 30 | super.setUp() 31 | mockProject = Mockito.mock(Project::class.java) 32 | mockEditor = Mockito.mock(Editor::class.java) 33 | mockLafManager = Mockito.mock(LafManager::class.java) 34 | mockTheme = Mockito.mock(UIThemeLookAndFeelInfo::class.java) 35 | 36 | // Mock the LafManager.getInstance() static method 37 | mockLafManagerStatic = Mockito.mockStatic(LafManager::class.java) 38 | mockLafManagerStatic.`when` { LafManager.getInstance() }.thenReturn(mockLafManager) 39 | 40 | // Mock the currentUIThemeLookAndFeel property 41 | Mockito.`when`(mockLafManager.currentUIThemeLookAndFeel).thenReturn(mockTheme) 42 | 43 | // Mock the isDark property 44 | Mockito.`when`(mockTheme.isDark).thenReturn(true) 45 | 46 | // Mock the necessary methods of the Editor class 47 | Mockito.`when`(mockEditor.project).thenReturn(mockProject) 48 | 49 | // Create a mock configuration 50 | val mockConfig = Mockito.mock(Events.Config.UpdatePayload::class.java) 51 | Mockito.`when`(mockEditor.getUserConfig()).thenReturn(mockConfig) 52 | } 53 | 54 | override fun tearDown() { 55 | // Close the static mock to prevent memory leaks 56 | mockLafManagerStatic.close() 57 | super.tearDown() 58 | } 59 | 60 | @Test 61 | fun testBrowserInitializationRaceCondition() { 62 | // Create a ChatWebView instance with the mocked editor 63 | val chatWebView = ChatWebView(mockEditor) { /* message handler */ } 64 | 65 | // First test with valid theme - should not throw 66 | try { 67 | chatWebView.setStyle() 68 | } catch (exception: Exception) { 69 | Assert.fail("Exception should not have been thrown: ${exception.message}") 70 | } 71 | // Force disposal while JavaScript might still be executing 72 | Thread.sleep(100) // Small delay to ensure the coroutine has started 73 | chatWebView.dispose() 74 | } 75 | 76 | @Test @Ignore("fails in ci") 77 | fun testSetupReactRaceCondition() { 78 | val chatWebView = ChatWebView(mockEditor) { /* message handler */ } 79 | try { 80 | chatWebView.setUpReact(chatWebView.webView.cefBrowser) 81 | } catch (exception: Exception) { 82 | Assert.fail("Exception should not have been thrown: ${exception.message}") 83 | } 84 | Thread.sleep(100) // Small delay to ensure the coroutine has started 85 | chatWebView.dispose() 86 | } 87 | 88 | @Test @Ignore("fails in ci") 89 | fun testPostMessageRaceCondition() { 90 | val chatWebView = ChatWebView(mockEditor) { /* message handler */ } 91 | try { 92 | chatWebView.postMessage("hello") 93 | // Just test with a string message 94 | chatWebView.postMessage("{\"type\": \"chat_message\", \"payload\": {\"message\": \"test message\"}}") 95 | } catch (exception: Exception) { 96 | Assert.fail("Exception should not have been thrown: ${exception.message}") 97 | } 98 | Thread.sleep(100) // Small delay to ensure the coroutine has started 99 | chatWebView.dispose() 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------