├── .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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
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 |
3 |
4 |
5 | ---
6 |
7 | [](https://smallcloud.ai/discord)
8 | [](https://twitter.com/intent/follow?screen_name=refact_ai)
9 | 
10 |
11 |
12 | # Refact-intellij
13 | *Refact for JetBrains is a free, open-source AI code assistant*
14 |
15 | ## Features
16 |
17 | - Access to 20+ LLMs: Leverage powerful language models, including GPT-4, Refact/1.6B, Code Llama, StarCoder2, Mistral, Mixtral, and more. Some models offer the ability to fine-tune for specialized needs.
18 |
19 | - CodeLens Integration: Get detailed insights into your code directly from your IDE using CodeLens. This feature enhances code navigation and understanding- CodeLens Integration: As you write code, you can instantly call a chat to find bugs or ask for an explanation of any code snippet.
20 |
21 | - FIM Debug Page: Access debugging tools and insights through the FIM debug page for improved troubleshooting.
22 |
23 | - Upload Images within Your IDE : Save time with our new in-IDE image upload feature—seamlessly integrated for your convenience. Easily upload one or multiple images to streamline your workflow
24 |
25 |
26 | ## Getting Started
27 | Once [installed](https://plugins.jetbrains.com/plugin/20647-refact-ai), look for the Refact.ai logo in the status bar or the sidebar, click 'login', and agree to T&C. Start typing some code, and autocomplete will make suggestions automatically! Press F1 to access the AI toolbox functions. Refact has a simple, user-friendly interface that makes it easy to use, even for those new to AI tools.
28 |
29 | If you have your own NVIDIA GPU, you can try the [self-hosted version](https://github.com/smallcloudai/refact).
30 | ## Support & Feedback
31 | Join our Discord to get to know other community members, send us feedback or suggestions, and get support.
32 |
--------------------------------------------------------------------------------
/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 |