Skip to content
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

refactor: Adjust inlay rendering and improve completion logic #3494

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -22,4 +22,4 @@ class AcceptAutocompleteAction : EditorAction(object : EditorActionHandler() {
&& autocompleteService.pendingCompletion?.text != null
return enabled
}
}) {}
})
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.InlayProperties
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.wm.WindowManager
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
Expand All @@ -32,9 +34,24 @@ fun PsiElement.isInjectedText(): Boolean {
return false
}

fun Editor.addInlayElement(
lines: List<String>,
offset: Int,
properties: InlayProperties
) {
if (this is EditorImpl) {
if (lines[0].isNotEmpty()) {
inlayModel.addInlineElement(offset, properties, ContinueInlayRenderer(listOf(lines[0])))
}
if (lines.size > 1) {
inlayModel.addBlockElement(offset, properties, ContinueInlayRenderer(lines.drop(1)))
}
}
}

@Service(Service.Level.PROJECT)
class AutocompleteService(private val project: Project) {
var pendingCompletion: PendingCompletion? = null;
var pendingCompletion: PendingCompletion? = null
private val autocompleteLookupListener = project.service<AutocompleteLookupListener>()
private var widget: AutocompleteSpinnerWidget? = null

Expand Down Expand Up @@ -66,23 +83,20 @@ class AutocompleteService(private val project: Project) {

// Request a completion from the core
val virtualFile = FileDocumentManager.getInstance().getFile(editor.document)
val line = editor.caretModel.primaryCaret.logicalPosition.line
val column = editor.caretModel.primaryCaret.logicalPosition.column
val input = mapOf(
"completionId" to completionId,
"filepath" to virtualFile?.url,
"filepath" to virtualFile?.path,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why change url to path?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(was this the solution to a clear problem?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was just about to comment on it. I didn't pay attention to #3247, need to revert it here.

Because using File(URI(filepath)) will cause an error in Windows, I made temporary changes locally and submitted this by mistake.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uh, maybe it's better to fix it than revert it.

"pos" to mapOf(
"line" to editor.caretModel.primaryCaret.logicalPosition.line,
"line" to line,
"character" to column
),
"recentlyEditedFiles" to emptyList<String>(),
"recentlyEditedRanges" to emptyList<String>(),
"clipboardText" to ""
)

val lineStart = editor.document.getLineStartOffset(editor.caretModel.primaryCaret.logicalPosition.line)
val lineEnd = editor.document.getLineEndOffset(editor.caretModel.primaryCaret.logicalPosition.line)
val lineLength = lineEnd - lineStart

project.service<ContinuePluginService>().coreMessenger?.request(
"autocomplete/complete",
input,
Expand All @@ -95,9 +109,8 @@ class AutocompleteService(private val project: Project) {
val completion = completions[0].toString()
val finalTextToInsert = deduplicateCompletion(editor, offset, completion)

if (shouldRenderCompletion(finalTextToInsert, column, lineLength, editor)) {
if (shouldRenderCompletion(finalTextToInsert, offset, line, editor)) {
renderCompletion(editor, offset, finalTextToInsert)
pendingCompletion = PendingCompletion(editor, offset, completionId, finalTextToInsert)
// Hide auto-popup
// AutoPopupController.getInstance(project).cancelAllRequests()
}
Expand All @@ -106,13 +119,19 @@ class AutocompleteService(private val project: Project) {
)
}

private fun shouldRenderCompletion(completion: String, column: Int, lineLength: Int, editor: Editor): Boolean {
if (completion.isEmpty()) {
private fun shouldRenderCompletion(completion: String, offset: Int, line: Int, editor: Editor): Boolean {
if (completion.isEmpty() || runReadAction { offset != editor.caretModel.offset }) {
return false
}

if (completion.lines().size == 1) {
return true
}

val endOffset = editor.document.getLineEndOffset(line)

// Do not render if completion is multi-line and caret is in middle of line
return !(completion.lines().size > 1 && column < lineLength)
return offset <= endOffset && editor.document.getText(TextRange(offset, endOffset)).isBlank()
}

private fun deduplicateCompletion(editor: Editor, offset: Int, completion: String): String {
Expand All @@ -122,9 +141,9 @@ class AutocompleteService(private val project: Project) {
val caretOffset = editor.caretModel.offset
val N = 10
var textAfterCursor = if (caretOffset + N <= document.textLength) {
document.getText(com.intellij.openapi.util.TextRange(caretOffset, caretOffset + N))
document.getText(TextRange(caretOffset, caretOffset + N))
} else {
document.getText(com.intellij.openapi.util.TextRange(caretOffset, document.textLength))
document.getText(TextRange(caretOffset, document.textLength))
}

// Determine the index of a newline character within the text following the cursor.
Expand Down Expand Up @@ -164,19 +183,9 @@ class AutocompleteService(private val project: Project) {
properties.relatesToPrecedingText(true)
properties.disableSoftWrapping(true)

if (completion.lines().size > 1) {
editor.inlayModel.addBlockElement(
offset,
properties,
ContinueMultilineCustomElementRenderer(editor, completion)
)
} else {
editor.inlayModel.addInlineElement(
offset,
properties,
ContinueCustomElementRenderer(editor, completion)
)
}
val lines = completion.lines()
pendingCompletion = pendingCompletion?.copy(text = lines.joinToString("\n"))
editor.addInlayElement(lines, offset, properties)

// val attributes = TextAttributes().apply {
// backgroundColor = JBColor.GREEN
Expand Down Expand Up @@ -204,7 +213,7 @@ class AutocompleteService(private val project: Project) {
({})
)
invokeLater {
clearCompletions(editor)
clearCompletions(editor, completion)
}
}

Expand Down Expand Up @@ -267,23 +276,14 @@ class AutocompleteService(private val project: Project) {
project.service<ContinuePluginService>().coreMessenger?.request("autocomplete/cancel", null, null, ({}))
}

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

if (pendingCompletion != null) {
cancelCompletion(pendingCompletion!!)
pendingCompletion = null
}
editor.inlayModel.getInlineElementsInRange(0, editor.document.textLength).forEach {
if (it.renderer is ContinueCustomElementRenderer) {
it.dispose()
}
}
editor.inlayModel.getBlockElementsInRange(0, editor.document.textLength).forEach {
if (it.renderer is ContinueMultilineCustomElementRenderer) {
it.dispose()
}
if (completion != null) {
cancelCompletion(completion)
if (completion.completionId == pendingCompletion?.completionId) pendingCompletion = null
}
disposeInlayRenderer(editor)
}

private fun isInjectedFile(editor: Editor): Boolean {
Expand All @@ -298,13 +298,17 @@ class AutocompleteService(private val project: Project) {
fun hideCompletions(editor: Editor) {
if (isInjectedFile(editor)) return

disposeInlayRenderer(editor)
}

private fun disposeInlayRenderer(editor: Editor) {
editor.inlayModel.getInlineElementsInRange(0, editor.document.textLength).forEach {
if (it.renderer is ContinueCustomElementRenderer) {
if (it.renderer is ContinueInlayRenderer) {
it.dispose()
}
}
editor.inlayModel.getBlockElementsInRange(0, editor.document.textLength).forEach {
if (it.renderer is ContinueMultilineCustomElementRenderer) {
if (it.renderer is ContinueInlayRenderer) {
it.dispose()
}
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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.colors.EditorFontType
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.ui.JBColor
import com.intellij.util.ui.UIUtil
import java.awt.Font
import java.awt.Graphics
import java.awt.Rectangle

/**
* The `ContinueInlayRenderer` class is responsible for rendering custom inlay elements within an editor.
* It implements the [EditorCustomElementRenderer] interface to provide custom rendering logic for inlays.
*
* This renderer is designed to display a list of text lines (`lines`) within the editor, calculating the
* necessary width and height based on the content and rendering each line with appropriate font and color.
*
* @author lk
*/
class ContinueInlayRenderer(val lines: List<String>) : EditorCustomElementRenderer {
override fun calcWidthInPixels(inlay: Inlay<*>): Int {
var maxLen = 0;
for (line in lines) {
val len = (inlay.editor as EditorImpl).getFontMetrics(Font.PLAIN).stringWidth(line)
if (len > maxLen) {
maxLen = len
}
}
return maxLen
}

override fun calcHeightInPixels(inlay: Inlay<*>): Int {
return (inlay.editor as EditorImpl).lineHeight * lines.size
}

private fun font(editor: Editor): Font {
val editorFont = editor.colorsScheme.getFont(EditorFontType.PLAIN)
return UIUtil.getFontWithFallbackIfNeeded(editorFont, lines.joinToString("\n"))
.deriveFont(editor.colorsScheme.editorFontSize)
}

override fun paint(inlay: Inlay<*>, g: Graphics, targetRegion: Rectangle, textAttributes: TextAttributes) {
val editor = inlay.editor
g.color = JBColor.GRAY
g.font = font(editor)
var additionalYOffset = 0
val ascent = editor.ascent
val lineHeight = editor.lineHeight
for (line in lines) {
g.drawString(line, targetRegion.x, targetRegion.y + ascent + additionalYOffset)
additionalYOffset += lineHeight
}
}
}

This file was deleted.

Loading