Skip to content

Jetbrains Autocomplete Crash Prevention #5825

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,23 @@ import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference

@Service(Service.Level.PROJECT)
class AutocompleteLookupListener(project: Project) : LookupManagerListener {
private val isLookupShown = AtomicBoolean(true)

private val activeLookup = AtomicReference<Lookup?>(null)

fun isLookupEmpty(): Boolean {
return isLookupShown.get()
return isLookupShown.get() && activeLookup.get() == null
}

init {
project.messageBus.connect().subscribe(LookupManagerListener.TOPIC, this)
}

override fun activeLookupChanged(oldLookup: Lookup?, newLookup: Lookup?) {
activeLookup.set(newLookup)
val newEditor = newLookup?.editor ?: return
if (newLookup is LookupImpl) {
newLookup.addLookupListener(
Expand All @@ -37,10 +40,12 @@ class AutocompleteLookupListener(project: Project) : LookupManagerListener {

override fun lookupCanceled(event: LookupEvent) {
isLookupShown.set(true)
activeLookup.set(null)
}

override fun itemSelected(event: LookupEvent) {
isLookupShown.set(true)
activeLookup.set(null)
}
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ fun Editor.addInlayElement(

@Service(Service.Level.PROJECT)
class AutocompleteService(private val project: Project) {
private val completionLock = Object()
var pendingCompletion: PendingCompletion? = null
private val autocompleteLookupListener = project.service<AutocompleteLookupListener>()
private val widget: AutocompleteSpinnerWidget? by lazy {
Expand All @@ -70,59 +71,61 @@ class AutocompleteService(private val project: Project) {
return
}

if (pendingCompletion != null) {
clearCompletions(pendingCompletion!!.editor)
}

// Set pending completion
val completionId = uuid()
val offset = editor.caretModel.primaryCaret.offset
pendingCompletion = PendingCompletion(editor, offset, completionId, null)

// Request a completion from the core
val virtualFile = FileDocumentManager.getInstance().getFile(editor.document)

val uri = virtualFile?.toUriOrNull() ?: return

widget?.setLoading(true)

val line = editor.caretModel.primaryCaret.logicalPosition.line
val column = editor.caretModel.primaryCaret.logicalPosition.column
val input = mapOf(
"completionId" to completionId,
"filepath" to uri,
"pos" to mapOf(
"line" to line,
"character" to column
),
"clipboardText" to "",
"recentlyEditedRanges" to emptyList<Any>(),
"recentlyVisitedRanges" to emptyList<Any>(),
)
synchronized(completionLock) {
if (pendingCompletion != null) {
clearCompletions(pendingCompletion!!.editor)
}

project.service<ContinuePluginService>().coreMessenger?.request(
"autocomplete/complete",
input,
null,
({ response ->
if (pendingCompletion == null || pendingCompletion?.completionId == completionId) {
widget?.setLoading(false)
}
// Set pending completion
val completionId = uuid()
val offset = editor.caretModel.primaryCaret.offset
pendingCompletion = PendingCompletion(editor, offset, completionId, null)

// Request a completion from the core
val virtualFile = FileDocumentManager.getInstance().getFile(editor.document)

val uri = virtualFile?.toUriOrNull() ?: return

widget?.setLoading(true)

val line = editor.caretModel.primaryCaret.logicalPosition.line
val column = editor.caretModel.primaryCaret.logicalPosition.column
val input = mapOf(
"completionId" to completionId,
"filepath" to uri,
"pos" to mapOf(
"line" to line,
"character" to column
),
"clipboardText" to "",
"recentlyEditedRanges" to emptyList<Any>(),
"recentlyVisitedRanges" to emptyList<Any>(),
)

project.service<ContinuePluginService>().coreMessenger?.request(
"autocomplete/complete",
input,
null,
({ response ->
if (pendingCompletion == null || pendingCompletion?.completionId == completionId) {
widget?.setLoading(false)
}

val responseObject = response as Map<*, *>
val completions = responseObject["content"] as List<*>
val responseObject = response as Map<*, *>
val completions = responseObject["content"] as List<*>

if (completions.isNotEmpty()) {
val completion = completions[0].toString()
val finalTextToInsert = deduplicateCompletion(editor, offset, completion)
if (completions.isNotEmpty()) {
val completion = completions[0].toString()
val finalTextToInsert = deduplicateCompletion(editor, offset, completion)

if (shouldRenderCompletion(finalTextToInsert, offset, line, editor)) {
renderCompletion(editor, offset, finalTextToInsert)
pendingCompletion = PendingCompletion(editor, offset, completionId, finalTextToInsert)
if (shouldRenderCompletion(finalTextToInsert, offset, line, editor)) {
renderCompletion(editor, offset, finalTextToInsert)
pendingCompletion = PendingCompletion(editor, offset, completionId, finalTextToInsert)
}
}
}
})
)
})
)
}
}

private fun shouldRenderCompletion(completion: String, offset: Int, line: Int, editor: Editor): Boolean {
Expand Down Expand Up @@ -178,34 +181,33 @@ class AutocompleteService(private val project: Project) {
}

private fun renderCompletion(editor: Editor, offset: Int, completion: String) {
if (completion.isEmpty()) {
return
}
if (isInjectedFile(editor)) return
// Skip rendering completions if the code completion dropdown is already visible and the IDE completion side-by-side setting is disabled
if (shouldSkipRender(ServiceManager.getService(ContinueExtensionSettings::class.java))) {
if (completion.isEmpty() || isInjectedFile(editor)) {
return
}

ApplicationManager.getApplication().invokeLater {
WriteAction.run<Throwable> {
// Clear existing completions
hideCompletions(editor)

val properties = InlayProperties()
properties.relatesToPrecedingText(true)
properties.disableSoftWrapping(true)

val lines = completion.lines()
pendingCompletion = pendingCompletion?.copy(text = lines.joinToString("\n"))
editor.addInlayElement(lines, offset, properties)

// val attributes = TextAttributes().apply {
// backgroundColor = JBColor.GREEN
// }
// val key = TextAttributesKey.createTextAttributesKey("CONTINUE_AUTOCOMPLETE")
// key.let { editor.colorsScheme.setAttributes(it, attributes) }
// editor.markupModel.addLineHighlighter(key, editor.caretModel.logicalPosition.line, HighlighterLayer.LAST)
synchronized(completionLock) {
if (ContinueInlayRenderer.hasConflictingInlays(editor, offset)) {
return@invokeLater
}

WriteAction.runAndWait<Throwable> {
try {
// Clear existing completions
hideCompletions(editor)

val properties = InlayProperties()
properties.relatesToPrecedingText(true)
properties.disableSoftWrapping(true)

val lines = completion.lines()
pendingCompletion = pendingCompletion?.copy(text = lines.joinToString("\n"))
editor.addInlayElement(lines, offset, properties)
} catch (e: Exception) {
// Log error and clean up
clearCompletions(editor)
}
}
}
}
}
Expand Down Expand Up @@ -290,13 +292,22 @@ class AutocompleteService(private val project: Project) {
}

fun clearCompletions(editor: Editor, completion: PendingCompletion? = pendingCompletion) {
if (isInjectedFile(editor)) return
synchronized(completionLock) {
if (isInjectedFile(editor)) return

if (completion != null) {
cancelCompletion(completion)
if (completion.completionId == pendingCompletion?.completionId) pendingCompletion = null
if (completion != null) {
cancelCompletion(completion)
if (completion.completionId == pendingCompletion?.completionId) {
pendingCompletion = null
}
}

ApplicationManager.getApplication().invokeLater {
WriteAction.runAndWait<Throwable> {
disposeInlayRenderer(editor)
}
}
}
disposeInlayRenderer(editor)
}

private fun isInjectedFile(editor: Editor): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.github.continuedev.continueintellijextension.autocomplete
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.EditorCustomElementRenderer
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.InlayModel
import com.intellij.openapi.editor.colors.EditorFontType
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.editor.markup.TextAttributes
Expand All @@ -22,6 +23,14 @@ import java.awt.Rectangle
* @author lk
*/
class ContinueInlayRenderer(val lines: List<String>) : EditorCustomElementRenderer {
companion object {
fun hasConflictingInlays(editor: Editor, offset: Int): Boolean {
val inlayModel = editor.inlayModel
val existingInlays = inlayModel.getInlineElementsInRange(offset, offset)
return existingInlays.any { it.renderer !is ContinueInlayRenderer }
}
}

override fun calcWidthInPixels(inlay: Inlay<*>): Int {
var maxLen = 0;
for (line in lines) {
Expand All @@ -45,6 +54,12 @@ class ContinueInlayRenderer(val lines: List<String>) : EditorCustomElementRender

override fun paint(inlay: Inlay<*>, g: Graphics, targetRegion: Rectangle, textAttributes: TextAttributes) {
val editor = inlay.editor

// Check for conflicts before rendering
if (hasConflictingInlays(editor, inlay.offset)) {
inlay.dispose()
return
}
g.color = JBColor.GRAY
g.font = font(editor)
var additionalYOffset = 0
Expand Down
Loading