feat: improved multi-line edits (#923)

* fix: llama.cpp copy task

* feat: next edit predictions

* feat: minor clean up
This commit is contained in:
Carl-Robert Linnupuu 2025-03-17 15:32:08 +00:00
parent 999d25fc85
commit e107d3a90b
23 changed files with 346 additions and 342 deletions

View file

@ -24,6 +24,7 @@ fun environment(key: String) = providers.environmentVariable(key)
plugins {
id("codegpt.java-conventions")
alias(libs.plugins.changelog)
alias(libs.plugins.protobuf)
}
group = properties("pluginGroup").get()
@ -67,6 +68,9 @@ dependencies {
implementation(libs.jsoup)
implementation(libs.commons.text)
implementation(libs.jtokkit)
implementation(libs.grpc.protobuf)
implementation(libs.grpc.stub)
implementation(libs.grpc.netty.shaded)
testImplementation(kotlin("test"))
}
@ -162,3 +166,22 @@ tasks {
}
}
}
protobuf {
protoc {
artifact = libs.protobuf.protoc.get().toString()
}
plugins {
create("grpc") {
artifact = libs.protobuf.java.get().toString()
}
}
generateProtoTasks {
all()
.forEach {
it.plugins {
create("grpc")
}
}
}
}

View file

@ -20,6 +20,7 @@ import java.util.List;
public class TelemetryConfiguration extends CompositeConfiguration {
public static final String KEY_MODE = "ee.carlrobert.telemetry.mode";
public static final String KEY_COMPLETION_TELEMETRY_ENABLED = "ee.carlrobert.telemetry.completionStatisticsEnabled";
private static final SaveableFileConfiguration FILE = new SaveableFileConfiguration(
Directories.PATH.resolve("ee.carlrobert.intellij.telemetry"));
@ -50,6 +51,14 @@ public class TelemetryConfiguration extends CompositeConfiguration {
setMode(Mode.valueOf(enabled));
}
public boolean isCompletionTelemetryEnabled() {
return Boolean.parseBoolean(get(KEY_COMPLETION_TELEMETRY_ENABLED));
}
public void setCompletionTelemetryEnabled(boolean enabled) {
put(KEY_COMPLETION_TELEMETRY_ENABLED, String.valueOf(enabled));
}
public boolean isDebug() {
return getMode() == Mode.DEBUG;
}

View file

@ -21,19 +21,27 @@ import javax.swing.JPanel;
*/
public class TelemetryComponent {
private static final String DESCRIPTION =
private static final String USAGE_DESCRIPTION =
"Help ProxyAI improve its products by sending anonymous data about features and plugins used, "
+ "hardware and software configuration.<br/>"
+ "<br/>"
+ "Please note that this will not include personal data or any sensitive Information.<br/>"
+ "Please note that this will not include personal data or any sensitive Information.";
private static final String COMPLETION_DESCRIPTION =
"Help ProxyAI improve its products by sharing your code inputs and completions.<br/>"
+ "<br/>"
+ "The data sent complies with the <a href=\"https://tryproxy.io/privacy\">Privacy Policy</a>.";
private final JPanel panel;
private final JBCheckBox enabled = new JBCheckBox("Send usage statistics");
private final JBCheckBox usageStatisticsEnabled = new JBCheckBox("Send usage statistics");
private final JBCheckBox completionStatisticsEnabled = new JBCheckBox("Send completion statistics");
public TelemetryComponent() {
this.panel = FormBuilder.createFormBuilder()
.addComponent(createCommentedPanel(enabled, DESCRIPTION), 1)
.addComponent(
createCommentedPanel(usageStatisticsEnabled, USAGE_DESCRIPTION), 1)
.addComponent(
createCommentedPanel(completionStatisticsEnabled, COMPLETION_DESCRIPTION), 1)
.addComponentFillVertically(new JPanel(), 0)
.getPanel();
}
@ -50,15 +58,22 @@ public class TelemetryComponent {
}
public JComponent getPreferredFocusedComponent() {
return enabled;
return usageStatisticsEnabled;
}
public boolean isEnabled() {
return enabled.isSelected();
public boolean getUsageStatisticsEnabled() {
return usageStatisticsEnabled.isSelected();
}
public void setEnabled(boolean enabled) {
this.enabled.setSelected(enabled);
public void setUsageStatisticsEnabled(boolean usageStatisticsEnabled) {
this.usageStatisticsEnabled.setSelected(usageStatisticsEnabled);
}
public boolean getCompletionStatisticsEnabled() {
return completionStatisticsEnabled.isSelected();
}
public void setCompletionStatisticsEnabled(boolean completionStatisticsEnabled) {
this.completionStatisticsEnabled.setSelected(completionStatisticsEnabled);
}
}

View file

@ -53,13 +53,15 @@ public class TelemetryConfigurable implements SearchableConfigurable {
@Override
public boolean isModified() {
boolean modified = false;
modified |= (component.isEnabled() != configuration.isEnabled());
modified |= (component.getUsageStatisticsEnabled() != configuration.isEnabled());
modified |= (component.getCompletionStatisticsEnabled() != configuration.isCompletionTelemetryEnabled());
return modified;
}
@Override
public void apply() {
configuration.setEnabled(component.isEnabled());
configuration.setEnabled(component.getUsageStatisticsEnabled());
configuration.setCompletionTelemetryEnabled(component.getCompletionStatisticsEnabled());
try {
configuration.save();
} catch (IOException e) {
@ -69,7 +71,8 @@ public class TelemetryConfigurable implements SearchableConfigurable {
@Override
public void reset() {
component.setEnabled(configuration.isEnabled());
component.setUsageStatisticsEnabled(configuration.isEnabled());
component.setCompletionStatisticsEnabled(configuration.isCompletionTelemetryEnabled());
}
@Override

View file

@ -9,4 +9,8 @@
files="[\\/]src[\\/]main[\\/]java[\\/]ee[\\/]carlrobert[\\/]codegpt[\\/]telemetry"
checks="."/>
<suppress files="[\\/]src[\\/]main[\\/]java[\\/]grammar" checks="."/>
<suppress
files="[\\/]build[\\/]generated[\\/]source[\\/]proto[\\/]main"
checks="."/>
</suppressions>

View file

@ -15,6 +15,9 @@ kotlin = "2.0.0"
llm-client = "0.8.37"
okio = "3.9.0"
tree-sitter = "0.24.4"
grpc = "1.71.0"
protobuf = "3.25.1"
protobuf-plugin = "0.9.4"
[libraries]
analytics = { module = "com.rudderstack.sdk.java.analytics:analytics", version.ref = "analytics" }
@ -31,6 +34,12 @@ kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", v
llm-client = { module = "ee.carlrobert:llm-client", version.ref = "llm-client" }
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
tree-sitter = { module = "io.github.bonede:tree-sitter", version.ref = "tree-sitter" }
grpc-protobuf = { module = "io.grpc:grpc-protobuf", version.ref = "grpc" }
grpc-stub = { module = "io.grpc:grpc-stub", version.ref = "grpc" }
grpc-netty-shaded = { module = "io.grpc:grpc-netty-shaded", version.ref = "grpc" }
protobuf-protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" }
protobuf-java = { module = "io.grpc:protoc-gen-grpc-java", version.ref = "grpc" }
[plugins]
changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" }
protobuf = { id = "com.google.protobuf", version.ref = "protobuf-plugin" }

View file

@ -1,76 +0,0 @@
package ee.carlrobert.codegpt
import com.intellij.codeInsight.editorActions.CopyPastePreProcessor
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.RawText
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CodeGptApiKey
import ee.carlrobert.codegpt.credentials.CredentialsStore.isCredentialSet
import ee.carlrobert.codegpt.predictions.PredictionService
import ee.carlrobert.codegpt.settings.GeneralSettings
import ee.carlrobert.codegpt.settings.service.ServiceType
import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class CodeGPTCopyPastePreProcessor : CopyPastePreProcessor {
private companion object {
const val MAX_TOKEN_LIMIT = 4096
const val FREE_TIER_TOKEN_LIMIT = 2048
private val logger = thisLogger()
}
override fun preprocessOnCopy(
file: PsiFile,
startOffsets: IntArray,
endOffsets: IntArray,
text: String
): String {
return text
}
override fun preprocessOnPaste(
project: Project,
file: PsiFile,
editor: Editor,
text: String,
rawText: RawText?
): String {
try {
displayPrediction(editor.document.text) {
service<PredictionService>().displayPastePrediction(editor, text)
}
} catch (e: Exception) {
logger.error("Error displaying paste prediction")
}
return text
}
private fun displayPrediction(documentText: String, handleDisplay: () -> Unit = {}) {
if (!isPredictionEnabled()) return
val currentTokens = getDocumentTokenCount(documentText)
if (currentTokens > MAX_TOKEN_LIMIT) return
if (!isCredentialSet(CodeGptApiKey) && currentTokens > FREE_TIER_TOKEN_LIMIT) return
CoroutineScope(Dispatchers.IO).launch {
handleDisplay()
}
}
private fun isPredictionEnabled(): Boolean =
GeneralSettings.getSelectedService() == ServiceType.CODEGPT &&
service<CodeGPTServiceSettings>().state.codeAssistantEnabled
private fun getDocumentTokenCount(text: String): Int =
EncodingManager.getInstance().countTokens(text)
}

View file

@ -8,8 +8,6 @@ import com.intellij.codeInsight.lookup.impl.LookupImpl
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.components.service
import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CodeGptApiKey
import ee.carlrobert.codegpt.credentials.CredentialsStore.isCredentialSet
import ee.carlrobert.codegpt.predictions.PredictionService
import ee.carlrobert.codegpt.settings.GeneralSettings
import ee.carlrobert.codegpt.settings.service.ServiceType
@ -34,22 +32,14 @@ class CodeGPTLookupListener : LookupManagerListener {
override fun itemSelected(event: LookupEvent) {
val editor = newLookup.editor
val encodingManager = EncodingManager.getInstance()
if (GeneralSettings.getSelectedService() != ServiceType.CODEGPT
|| !service<CodeGPTServiceSettings>().state.codeAssistantEnabled
|| encodingManager.countTokens(editor.document.text) > 4096
|| !isCredentialSet(CodeGptApiKey) && encodingManager.countTokens(editor.document.text) > 2048
|| !service<CodeGPTServiceSettings>().state.nextEditsEnabled
) {
return
}
ApplicationManager.getApplication().executeOnPooledThread {
service<PredictionService>().displayLookupPrediction(
editor,
event,
beforeApply,
cursorOffset
)
service<PredictionService>().displayInlineDiff(editor)
}
}
})

View file

@ -14,11 +14,11 @@ abstract class CodeAssistantFeatureToggleAction(
override fun actionPerformed(e: AnActionEvent) {
val settings = service<CodeGPTServiceSettings>().state
settings.codeAssistantEnabled = enableFeatureAction
settings.nextEditsEnabled = enableFeatureAction
}
override fun update(e: AnActionEvent) {
val codeAssistantEnabled = service<CodeGPTServiceSettings>().state.codeAssistantEnabled
val codeAssistantEnabled = service<CodeGPTServiceSettings>().state.nextEditsEnabled
e.presentation.isVisible = GeneralSettings.getSelectedService() == CODEGPT
&& codeAssistantEnabled != enableFeatureAction
@ -30,6 +30,6 @@ abstract class CodeAssistantFeatureToggleAction(
}
}
class EnableCodeAssistantAction : CodeAssistantFeatureToggleAction(true)
class EnableNextEditsAction : CodeAssistantFeatureToggleAction(true)
class DisableCodeAssistantAction : CodeAssistantFeatureToggleAction(false)
class DisableNextEditsAction : CodeAssistantFeatureToggleAction(false)

View file

@ -37,7 +37,8 @@ abstract class CodeCompletionFeatureToggleActions(
override fun update(e: AnActionEvent) {
val selectedService = GeneralSettings.getSelectedService()
val codeCompletionEnabled =
e.project?.service<CodeCompletionService>()?.isCodeCompletionsEnabled(selectedService) ?: false
e.project?.service<CodeCompletionService>()?.isCodeCompletionsEnabled(selectedService)
?: false
e.presentation.isVisible = codeCompletionEnabled != enableFeatureAction
e.presentation.isEnabled = when (selectedService) {
CODEGPT,
@ -60,4 +61,4 @@ abstract class CodeCompletionFeatureToggleActions(
class EnableCompletionsAction : CodeCompletionFeatureToggleActions(true)
class DisableCompletionsAction : CodeCompletionFeatureToggleActions(false)
class DisableCompletionsAction : CodeCompletionFeatureToggleActions(false)

View file

@ -0,0 +1,179 @@
package ee.carlrobert.codegpt.codecompletions.edit
import com.intellij.notification.NotificationAction.createSimpleExpiring
import com.intellij.notification.NotificationType
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.jetbrains.rd.util.UUID
import ee.carlrobert.codegpt.credentials.CredentialsStore
import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CodeGptApiKey
import ee.carlrobert.codegpt.predictions.CodeSuggestionDiffViewer
import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings
import ee.carlrobert.codegpt.telemetry.core.configuration.TelemetryConfiguration
import ee.carlrobert.codegpt.ui.OverlayUtil
import ee.carlrobert.codegpt.util.GitUtil
import ee.carlrobert.service.AcceptEditRequest
import ee.carlrobert.service.NextEditRequest
import ee.carlrobert.service.NextEditResponse
import ee.carlrobert.service.NextEditServiceImplGrpc
import io.grpc.*
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder
import io.grpc.stub.StreamObserver
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
import kotlin.coroutines.cancellation.CancellationException
@Service(Service.Level.PROJECT)
class GrpcClientService(private val project: Project) : Disposable {
private var channel: ManagedChannel? = null
private var stub: NextEditServiceImplGrpc.NextEditServiceImplStub? = null
private var prevObserver: NextEditStreamObserver? = null
companion object {
private const val HOST = "grpc.tryproxy.io"
private const val PORT = 9090
private const val SHUTDOWN_TIMEOUT_SECONDS = 5L
private val logger = thisLogger()
}
@Synchronized
private fun ensureConnection() {
if (channel == null || channel?.isShutdown == true) {
try {
channel = NettyChannelBuilder.forAddress(HOST, PORT).build()
stub = NextEditServiceImplGrpc.newStub(channel)
.withCallCredentials(
ApiKeyCredentials(CredentialsStore.getCredential(CodeGptApiKey) ?: "")
)
logger.info("gRPC connection established")
} catch (e: Exception) {
logger.error("Failed to establish gRPC connection", e)
throw e
}
}
}
fun getNextEdit(editor: Editor, isManuallyOpened: Boolean = false) {
ensureConnection()
prevObserver?.onCompleted()
val request = NextEditRequest.newBuilder()
.setFileName(editor.virtualFile.name)
.setFileContent(editor.document.text)
.setGitDiff(GitUtil.getCurrentChanges(project) ?: "")
.setCursorPosition(runReadAction { editor.caretModel.offset })
.setEnableTelemetry(TelemetryConfiguration.getInstance().isCompletionTelemetryEnabled)
.build()
prevObserver = NextEditStreamObserver(editor, isManuallyOpened) {
dispose()
}
stub?.nextEdit(request, prevObserver)
}
class NextEditStreamObserver(
private val editor: Editor,
private val isManuallyOpened: Boolean,
private val onDispose: () -> Unit
) : StreamObserver<NextEditResponse> {
override fun onNext(response: NextEditResponse) {
runInEdt {
CodeSuggestionDiffViewer.displayInlineDiff(editor, response, isManuallyOpened)
}
}
override fun onError(ex: Throwable) {
if (ex is CancellationException) {
onCompleted()
return
}
try {
if (ex is StatusRuntimeException) {
if (ex.status.code == Status.Code.CANCELLED) {
onCompleted()
return
}
OverlayUtil.showNotification(
ex.status.description ?: ex.localizedMessage,
NotificationType.ERROR,
createSimpleExpiring("Disable multi-line edits") {
service<CodeGPTServiceSettings>().state.nextEditsEnabled =
false
})
} else {
logger.error("Something went wrong", ex)
}
} finally {
onDispose()
}
}
override fun onCompleted() {
}
}
fun acceptEdit(responseId: UUID, acceptedEdit: String) {
NextEditServiceImplGrpc
.newBlockingStub(channel)
.acceptEdit(
AcceptEditRequest.newBuilder()
.setResponseId(responseId.toString())
.setAcceptedEdit(acceptedEdit)
.build()
)
}
override fun dispose() {
channel?.let { ch ->
if (!ch.isShutdown) {
try {
ch.shutdown().awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)
logger.info("gRPC connection closed")
} catch (e: InterruptedException) {
logger.warn("Interrupted while shutting down gRPC channel", e)
Thread.currentThread().interrupt()
} finally {
if (!ch.isTerminated) {
ch.shutdownNow()
}
channel = null
}
}
}
}
}
internal class ApiKeyCredentials(private val apiKey: String) : CallCredentials() {
companion object {
private val API_KEY_HEADER: Metadata.Key<String> =
Metadata.Key.of("x-api-key", Metadata.ASCII_STRING_MARSHALLER)
}
override fun applyRequestMetadata(
requestInfo: RequestInfo?,
executor: Executor,
metadataApplier: MetadataApplier
) {
executor.execute {
try {
val headers = Metadata()
headers.put(API_KEY_HEADER, apiKey)
metadataApplier.apply(headers)
} catch (e: Throwable) {
metadataApplier.fail(Status.UNAUTHENTICATED.withCause(e))
}
}
}
}

View file

@ -27,15 +27,19 @@ import com.intellij.openapi.util.*
import com.intellij.testFramework.LightVirtualFile
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBScrollPane
import com.intellij.util.application
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.components.BorderLayoutPanel
import ee.carlrobert.codegpt.CodeGPTBundle
import ee.carlrobert.codegpt.CodeGPTKeys
import ee.carlrobert.codegpt.codecompletions.edit.GrpcClientService
import ee.carlrobert.service.NextEditResponse
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.FlowLayout
import java.awt.Point
import java.util.*
import javax.swing.Box
import javax.swing.JComponent
import javax.swing.JPanel
@ -45,6 +49,7 @@ import kotlin.math.max
class CodeSuggestionDiffViewer(
request: DiffRequest,
private val responseId: UUID,
private val mainEditor: Editor,
private val isManuallyOpened: Boolean
) : UnifiedDiffViewer(MyDiffContext(mainEditor.project), request), Disposable {
@ -117,6 +122,10 @@ class CodeSuggestionDiffViewer(
if (changes.size == 1) {
popup.dispose()
}
application.executeOnPooledThread {
project?.service<GrpcClientService>()?.acceptEdit(responseId, change.toString())
}
}
fun isVisible(): Boolean {
@ -288,17 +297,23 @@ class CodeSuggestionDiffViewer(
@RequiresEdt
fun displayInlineDiff(
editor: Editor,
nextRevision: String,
nextEditResponse: NextEditResponse,
isManuallyOpened: Boolean = false
) {
if ((editor is EditorEx && editor.virtualFile == null) || editor.isViewer) {
val nextRevision = nextEditResponse.nextRevision
if ((editor is EditorEx && editor.virtualFile == null) || editor.isViewer || nextRevision.isEmpty()) {
return
}
editor.getUserData(CodeGPTKeys.EDITOR_PREDICTION_DIFF_VIEWER)?.dispose()
val diffRequest = createSimpleDiffRequest(editor, nextRevision)
val diffViewer = CodeSuggestionDiffViewer(diffRequest, editor, isManuallyOpened)
val diffViewer = CodeSuggestionDiffViewer(
diffRequest,
UUID.fromString(nextEditResponse.id),
editor,
isManuallyOpened
)
editor.putUserData(CodeGPTKeys.EDITOR_PREDICTION_DIFF_VIEWER, diffViewer)
diffViewer.rediff(true)
}

View file

@ -1,76 +1,23 @@
package ee.carlrobert.codegpt.predictions
import com.intellij.codeInsight.lookup.LookupEvent
import com.intellij.diff.DiffManager
import com.intellij.notification.NotificationListener
import com.intellij.notification.NotificationType
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.project.Project
import com.intellij.testFramework.LightVirtualFile
import ee.carlrobert.codegpt.CodeGPTKeys
import ee.carlrobert.codegpt.CodeGPTKeys.PENDING_PREDICTION_CALL
import ee.carlrobert.codegpt.completions.CompletionClientProvider
import ee.carlrobert.codegpt.conversations.ConversationsState
import ee.carlrobert.codegpt.settings.prompts.PromptsSettings
import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings
import ee.carlrobert.codegpt.ui.OverlayUtil
import ee.carlrobert.codegpt.ui.OverlayUtil.getDefaultNotification
import ee.carlrobert.codegpt.codecompletions.edit.GrpcClientService
import ee.carlrobert.codegpt.util.EditorDiffUtil.createDiffRequest
import ee.carlrobert.codegpt.util.EditorUtil
import ee.carlrobert.codegpt.util.GitUtil
import ee.carlrobert.llm.client.codegpt.request.prediction.AutocompletionPredictionRequest
import ee.carlrobert.llm.client.codegpt.request.prediction.DirectPredictionRequest
import ee.carlrobert.llm.client.codegpt.request.prediction.PastePredictionRequest
import ee.carlrobert.llm.client.codegpt.request.prediction.PredictionRequest
import ee.carlrobert.llm.client.codegpt.response.CodeGPTException
import ee.carlrobert.llm.client.codegpt.response.PredictionResponse
import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionStandardMessage
import okhttp3.Request
@Service
class PredictionService {
fun displayDirectPrediction(editor: Editor) {
val request = CompletionClientProvider.getCodeGPTClient()
.buildDirectPredictionRequest(createDirectPredictionRequest(editor))
displayInlineDiff(editor, request, true)
}
fun displayAutocompletePrediction(editor: Editor, textToInsert: String, beforeApply: String) {
val request = CompletionClientProvider.getCodeGPTClient()
.buildAutocompletionPredictionRequest(
createAutocompletePredictionRequest(editor, textToInsert, beforeApply)
)
displayInlineDiff(editor, request)
}
fun displayLookupPrediction(
editor: Editor,
event: LookupEvent,
beforeApply: String,
cursorOffset: Int
) {
val request = CompletionClientProvider.getCodeGPTClient()
.buildLookupPredictionRequest(
createAutocompletePredictionRequest(
editor,
event.item?.lookupString ?: "",
beforeApply,
cursorOffset
)
)
displayInlineDiff(editor, request)
}
fun displayPastePrediction(editor: Editor, pastedText: String) {
val request = CompletionClientProvider.getCodeGPTClient()
.buildPastePredictionRequest(createPastePredictionRequest(editor, pastedText))
displayInlineDiff(editor, request)
companion object {
private val logger = thisLogger()
}
fun acceptPrediction(editor: Editor) {
@ -81,95 +28,15 @@ class PredictionService {
}
}
private fun displayInlineDiff(
fun displayInlineDiff(
editor: Editor,
request: Request,
isManuallyOpened: Boolean = false
) {
val prediction = getPrediction(editor, request)
if (prediction != null && !prediction.nextRevision.isNullOrEmpty()) {
runInEdt {
CodeSuggestionDiffViewer.displayInlineDiff(
editor,
prediction.nextRevision,
isManuallyOpened
)
}
}
}
private fun getPrediction(editor: Editor, request: Request): PredictionResponse? {
val pendingCall = PENDING_PREDICTION_CALL.get(editor)
if (pendingCall != null) {
pendingCall.cancel()
return null
}
val project = editor.project ?: return
try {
val client = CompletionClientProvider.getCodeGPTClient()
val call = client.createNewCall(request)
PENDING_PREDICTION_CALL.set(editor, call)
return client.getPrediction(call)
} catch (e: CodeGPTException) {
OverlayUtil.notify(
getDefaultNotification(e.detail, NotificationType.ERROR).apply {
setListener(NotificationListener.UrlOpeningListener(true))
})
service<CodeGPTServiceSettings>().state.codeAssistantEnabled = false
return null
} catch (e: Exception) {
if (e.cause?.message != "Canceled") {
throw e
}
return null
} finally {
PENDING_PREDICTION_CALL.set(editor, null)
}
}
private fun createAutocompletePredictionRequest(
editor: Editor,
textToInsert: String,
beforeApply: String,
cursorOffset: Int? = null,
): AutocompletionPredictionRequest {
val predictionRequest = AutocompletionPredictionRequest()
predictionRequest.appliedCompletion = textToInsert
predictionRequest.previousRevision = beforeApply
setDefaultParams(editor, predictionRequest, cursorOffset)
return predictionRequest
}
private fun createPastePredictionRequest(
editor: Editor,
pastedCode: String
): PastePredictionRequest {
val predictionRequest = PastePredictionRequest(pastedCode)
setDefaultParams(editor, predictionRequest)
return predictionRequest
}
private fun createDirectPredictionRequest(editor: Editor): DirectPredictionRequest {
val predictionRequest = DirectPredictionRequest()
setDefaultParams(editor, predictionRequest)
return predictionRequest
}
private fun setDefaultParams(editor: Editor, request: PredictionRequest, offset: Int? = null) {
val messages: MutableList<OpenAIChatCompletionStandardMessage> = mutableListOf()
ConversationsState.getInstance().currentConversation?.messages?.forEach {
messages.add(OpenAIChatCompletionStandardMessage("user", it.prompt))
messages.add(OpenAIChatCompletionStandardMessage("assistant", it.response))
}
request.apply {
currentRevision = runReadAction { editor.document.text }
customPrompt =
service<PromptsSettings>().state.coreActions.codeAssistant.instructions
cursorOffset = offset ?: runReadAction { editor.caretModel.offset }
gitChanges = GitUtil.getCurrentChanges(editor.project!!)
openFiles = EditorUtil.getOpenFiles(editor.project!!)
conversationMessages = messages.toList()
project.service<GrpcClientService>().getNextEdit(editor, isManuallyOpened)
} catch (ex: Exception) {
logger.error("Error communicating with server: ${ex.message}")
}
}

View file

@ -12,9 +12,6 @@ import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.EditorAction
import com.intellij.openapi.editor.actionSystem.EditorWriteActionHandler
import ee.carlrobert.codegpt.CodeGPTKeys
import ee.carlrobert.codegpt.EncodingManager
import ee.carlrobert.codegpt.credentials.CredentialsStore.CredentialKey.CodeGptApiKey
import ee.carlrobert.codegpt.credentials.CredentialsStore.isCredentialSet
import ee.carlrobert.codegpt.settings.GeneralSettings
import ee.carlrobert.codegpt.settings.service.ServiceType
import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings
@ -33,14 +30,14 @@ class TriggerCustomPredictionAction : EditorAction(Handler()), HintManagerImpl.A
return
}
if (!service<CodeGPTServiceSettings>().state.codeAssistantEnabled) {
if (!service<CodeGPTServiceSettings>().state.nextEditsEnabled) {
val notification = OverlayUtil.getDefaultNotification(
"Please enable Code Assistant before using this feature.",
"Please enable multi-line edits before using this feature.",
NotificationType.WARNING,
)
notification.addAction(object : AnAction("Enable Code Assistant") {
notification.addAction(object : AnAction("Enable Multi-Line Edits") {
override fun actionPerformed(e: AnActionEvent) {
service<CodeGPTServiceSettings>().state.codeAssistantEnabled = true
service<CodeGPTServiceSettings>().state.nextEditsEnabled = true
notification.hideBalloon()
}
})
@ -49,18 +46,8 @@ class TriggerCustomPredictionAction : EditorAction(Handler()), HintManagerImpl.A
return
}
val encodingManager = service<EncodingManager>()
if (!isCredentialSet(CodeGptApiKey) && encodingManager.countTokens(editor.document.text) > 2048) {
OverlayUtil.showNotification("The file exceeds the token limit of 2,048. Please upgrade your plan to access higher limits.")
return
}
if (encodingManager.countTokens(editor.document.text) > 4096) {
OverlayUtil.showNotification("The file exceeds the token limit of 4,096.")
return
}
ApplicationManager.getApplication().executeOnPooledThread {
service<PredictionService>().displayDirectPrediction(editor)
service<PredictionService>().displayInlineDiff(editor, true)
}
}

View file

@ -30,8 +30,6 @@ class PromptsSettingsState : BaseState() {
class CoreActionsState : BaseState() {
companion object {
val DEFAULT_CODE_ASSISTANT_PROMPT =
getResourceContent("/prompts/core/code-assistant.txt")
val DEFAULT_EDIT_CODE_PROMPT = getResourceContent("/prompts/core/edit-code.txt")
val DEFAULT_GENERATE_COMMIT_MESSAGE_PROMPT =
getResourceContent("/prompts/core/generate-commit-message.txt")
@ -43,11 +41,6 @@ class CoreActionsState : BaseState() {
getResourceContent("/prompts/core/review-changes.txt")
}
var codeAssistant by property(CoreActionPromptDetailsState().apply {
name = "Code Assistant"
code = "CODE_ASSISTANT"
instructions = DEFAULT_CODE_ASSISTANT_PROMPT
})
var editCode by property(CoreActionPromptDetailsState().apply {
name = "Edit Code"
code = "EDIT_CODE"

View file

@ -104,7 +104,6 @@ class PromptsForm {
val coreActionsFormState = getFormState<CoreActionPromptDetails>(coreActionsNode)
settings.coreActions.apply {
codeAssistant = coreActionsFormState[0].toState()
editCode = coreActionsFormState[1].toState()
fixCompileErrors = coreActionsFormState[2].toState()
generateCommitMessage = coreActionsFormState[3].toState()
@ -159,7 +158,6 @@ class PromptsForm {
val formState = getFormState<CoreActionPromptDetails>(coreActionsNode)
val stateActions = listOf(
settingsState.codeAssistant,
settingsState.editCode,
settingsState.fixCompileErrors,
settingsState.generateCommitMessage,
@ -215,7 +213,6 @@ class PromptsForm {
val settings = service<PromptsSettings>().state
listOf(
settings.coreActions.codeAssistant,
settings.coreActions.editCode,
settings.coreActions.fixCompileErrors,
settings.coreActions.generateCommitMessage,

View file

@ -10,7 +10,6 @@ import com.intellij.ui.layout.ComponentPredicate
import com.intellij.util.ui.components.BorderLayoutPanel
import ee.carlrobert.codegpt.settings.configuration.Placeholder
import ee.carlrobert.codegpt.settings.prompts.CommitMessageTemplate
import ee.carlrobert.codegpt.settings.prompts.CoreActionsState.Companion.DEFAULT_CODE_ASSISTANT_PROMPT
import ee.carlrobert.codegpt.settings.prompts.CoreActionsState.Companion.DEFAULT_EDIT_CODE_PROMPT
import ee.carlrobert.codegpt.settings.prompts.CoreActionsState.Companion.DEFAULT_FIX_COMPILE_ERRORS_PROMPT
import ee.carlrobert.codegpt.settings.prompts.CoreActionsState.Companion.DEFAULT_GENERATE_COMMIT_MESSAGE_PROMPT
@ -29,26 +28,6 @@ class CoreActionsDetailsPanel : PromptDetailsPanel {
override fun create(details: CoreActionPromptDetails): JComponent {
val editorPanel = when (details.code) {
"CODE_ASSISTANT" -> CoreActionEditorPanel(
details,
DEFAULT_CODE_ASSISTANT_PROMPT,
buildString {
append("<p>Template for generating code assistant messages. Use the following placeholders to insert dynamic values:</p>\n")
append(
"<ul>${
listOf(
Placeholder.GIT_DIFF,
Placeholder.OPEN_FILES,
Placeholder.ACTIVE_CONVERSATION,
).joinToString("\n") {
"<li><strong>${it.name}</strong>: ${it.description}</li>"
}
}</ul>\n"
)
},
listOf("{GIT_DIFF}", "{OPEN_FILES}", "{ACTIVE_CONVERSATION}")
)
"EDIT_CODE" -> CoreActionEditorPanel(
details,
DEFAULT_EDIT_CODE_PROMPT,
@ -90,7 +69,6 @@ class CoreActionsDetailsPanel : PromptDetailsPanel {
init {
val settings = service<PromptsSettings>().state.coreActions
listOf(
settings.codeAssistant,
settings.editCode,
settings.fixCompileErrors,
settings.generateCommitMessage,

View file

@ -32,9 +32,9 @@ class CodeGPTServiceForm {
renderer = CustomComboBoxRenderer()
}
private val codeAssistantEnabledCheckBox = JBCheckBox(
CodeGPTBundle.get("shared.enableCodeAssistant"),
service<CodeGPTServiceSettings>().state.codeAssistantEnabled
private val enableNextEditsEnabledCheckBox = JBCheckBox(
"Enable multi-line edits",
service<CodeGPTServiceSettings>().state.nextEditsEnabled
)
private val codeCompletionsEnabledCheckBox = JBCheckBox(
@ -73,9 +73,9 @@ class CodeGPTServiceForm {
UIUtil.createComment("settingsConfigurable.service.codegpt.codeCompletionModel.comment")
)
.addVerticalGap(4)
.addComponent(codeAssistantEnabledCheckBox)
.addComponent(enableNextEditsEnabledCheckBox)
.addComponent(
UIUtil.createComment("settingsConfigurable.service.codegpt.enableCodeAssistant.comment", 90)
UIUtil.createComment("settingsConfigurable.service.codegpt.enableNextEdits.comment", 90)
)
.addVerticalGap(4)
.addComponent(codeCompletionsEnabledCheckBox)
@ -90,14 +90,14 @@ class CodeGPTServiceForm {
fun isModified() = service<CodeGPTServiceSettings>().state.run {
(chatCompletionModelComboBox.selectedItem as CodeGPTModel).code != chatCompletionSettings.model
|| (codeCompletionModelComboBox.selectedItem as CodeGPTModel).code != codeCompletionSettings.model
|| codeAssistantEnabledCheckBox.isSelected != codeAssistantEnabled
|| enableNextEditsEnabledCheckBox.isSelected != nextEditsEnabled
|| codeCompletionsEnabledCheckBox.isSelected != codeCompletionSettings.codeCompletionsEnabled
|| getApiKey() != getCredential(CodeGptApiKey)
}
fun applyChanges() {
service<CodeGPTServiceSettings>().state.run {
codeAssistantEnabled = codeAssistantEnabledCheckBox.isSelected
nextEditsEnabled = enableNextEditsEnabledCheckBox.isSelected
chatCompletionSettings.model =
(chatCompletionModelComboBox.selectedItem as CodeGPTModel).code
codeCompletionSettings.codeCompletionsEnabled =
@ -110,7 +110,7 @@ class CodeGPTServiceForm {
fun resetForm() {
service<CodeGPTServiceSettings>().state.run {
codeAssistantEnabledCheckBox.isSelected = codeAssistantEnabled
enableNextEditsEnabledCheckBox.isSelected = nextEditsEnabled
chatCompletionModelComboBox.selectedItem = chatCompletionSettings.model
codeCompletionModelComboBox.selectedItem = codeCompletionSettings.model
codeCompletionsEnabledCheckBox.isSelected =

View file

@ -13,7 +13,7 @@ class CodeGPTServiceSettings :
class CodeGPTServiceSettingsState : BaseState() {
var chatCompletionSettings by property(CodeGPTServiceChatCompletionSettingsState())
var codeCompletionSettings by property(CodeGPTServiceCodeCompletionSettingsState())
var codeAssistantEnabled by property(false)
var nextEditsEnabled by property(true)
}
class CodeGPTServiceChatCompletionSettingsState : BaseState() {

View file

@ -0,0 +1,29 @@
// src/main/proto/edit.proto
syntax = "proto3";
option java_multiple_files = true;
option java_package = "ee.carlrobert.service";
import "google/protobuf/empty.proto";
service NextEditServiceImpl {
rpc NextEdit (NextEditRequest) returns (stream NextEditResponse);
rpc AcceptEdit (AcceptEditRequest) returns (google.protobuf.Empty);
}
message NextEditRequest {
string file_content = 1;
string file_name = 2;
int32 cursor_position = 3;
string git_diff = 4;
bool enable_telemetry = 5;
}
message NextEditResponse {
string id = 1;
string next_revision = 2;
}
message AcceptEditRequest {
string response_id = 1;
string accepted_edit = 2;
}

View file

@ -211,16 +211,16 @@
<override-text place="popup" use-text-of-place="MainMenu"/>
</action>
<action
id="statusbar.enableCodeAssistant"
class="ee.carlrobert.codegpt.actions.EnableCodeAssistantAction">
id="statusbar.enableNextEdits"
class="ee.carlrobert.codegpt.actions.EnableNextEditsAction">
<keyboard-shortcut first-keystroke="ctrl shift alt a" keymap="$default"/>
<override-text place="MainMenu"/>
<override-text place="popup" use-text-of-place="MainMenu"/>
</action>
<action
id="statusbar.disableCodeAssistant"
class="ee.carlrobert.codegpt.actions.DisableCodeAssistantAction">
id="statusbar.disableNextEdits"
class="ee.carlrobert.codegpt.actions.DisableNextEditsAction">
<keyboard-shortcut first-keystroke="ctrl shift alt a" keymap="$default"/>
<override-text place="MainMenu"/>
@ -248,8 +248,8 @@
<separator/>
<reference id="statusbar.stopServer" />
<reference id="statusbar.startServer" />
<reference id="statusbar.disableCodeAssistant" />
<reference id="statusbar.enableCodeAssistant" />
<reference id="statusbar.disableNextEdits" />
<reference id="statusbar.enableNextEdits" />
<reference id="statusbar.disableCompletions" />
<reference id="statusbar.enableCompletions" />
</group>

View file

@ -25,12 +25,12 @@ action.statusbar.enableCompletions.MainMenu.text=Enable Completions
action.statusbar.disableCompletions.text=Disable Completions
action.statusbar.disableCompletions.description=Disable Code Completions
action.statusbar.disableCompletions.MainMenu.text=Disable Completions
action.statusbar.enableCodeAssistant.text=Enable Code Assistant
action.statusbar.enableCodeAssistant.description=Enable Code Assistant
action.statusbar.enableCodeAssistant.MainMenu.text=Enable Code Assistant
action.statusbar.disableCodeAssistant.text=Disable Code Assistant
action.statusbar.disableCodeAssistant.description=Disable Code Assistant
action.statusbar.disableCodeAssistant.MainMenu.text=Disable Code Assistant
action.statusbar.enableNextEdits.text=Enable Multi-Line Edits
action.statusbar.enableNextEdits.description=Enable Multi-Line Edits
action.statusbar.enableNextEdits.MainMenu.text=Enable Multi-Line Edits
action.statusbar.disableNextEdits.text=Disable Multi-Line Edits
action.statusbar.disableNextEdits.description=Disable Multi-Line Edits
action.statusbar.disableNextEdits.MainMenu.text=Disable Multi-Line Edits
action.compareWithOriginal.title=Compare with Original
action.applyDirectly.title=Auto Apply
action.explainGitCommit.title=Explain Commit with ProxyAI
@ -42,7 +42,7 @@ settingsConfigurable.service.label=Selected provider:
settingsConfigurable.service.codegpt.apiKey.comment=You can find the API key in your <a href="https://tryproxy.io/account">User settings</a>.
settingsConfigurable.service.codegpt.chatCompletionModel.comment=Choose a model optimized for conversational interactions, including assistance with general queries and explanations.
settingsConfigurable.service.codegpt.codeCompletionModel.comment=Choose a model tailored for code completion-related tasks.
settingsConfigurable.service.codegpt.enableCodeAssistant.comment=If checked, Code Assistant will suggest related code updates as you make changes.
settingsConfigurable.service.codegpt.enableNextEdits.comment=If checked, ProxyAI will suggest multi-line changes as you type.
settingsConfigurable.service.codegpt.enableCodeCompletion.comment=If checked, ProxyAI will suggest changes as you type.
settingsConfigurable.service.custom.openai.apiKey.comment=A secret value stored in the system's Keychain or KeePass, depending on your OS. This approach is recommended over storing the secret in the header as plain text.
settingsConfigurable.service.custom.openai.apiKey.provider.name=Custom provider name:
@ -287,7 +287,6 @@ imageAccordion.title=Attached image
shared.image=Image
shared.chatCompletions=Chat Completions
shared.codeCompletions=Code Completions
shared.enableCodeAssistant=<html>Enable Code Assistant <small><strong><sup style="color: green;">NEW</sup></strong></small></html>
codeCompletionsForm.enableFeatureText=Enable code completions
codeCompletionsForm.parseResponseAsChatCompletions=Parse response as Chat Completions
codeCompletionsForm.overrideFimTemplate.label=Use built-in FIM template

View file

@ -1,18 +0,0 @@
Recent changes made to the project:
<git_diff>
{GIT_DIFF}
</git_diff>
Currently open files:
<open_files>
{OPEN_FILES}
</open_files>
Previous chat history between the AI and user:
<active_conversation_history>
{ACTIVE_CONVERSATION}
</active_conversation_history>
Important Guidelines:
- Carefully follow the instructions marked with the @AI keyword and act accordingly. Then, delete the instruction.