Skip to content

Commit 4b97663

Browse files
authored
Merge pull request continuedev#3494 from lkk214/refactor/completion-inlay-rendering
refactor: Adjust inlay rendering and improve completion logic
2 parents 0c959f7 + 79736c3 commit 4b97663

File tree

9 files changed

+137
-189
lines changed

9 files changed

+137
-189
lines changed

extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/activities/ContinuePluginStartupActivity.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import com.github.continuedev.continueintellijextension.listeners.ContinuePlugin
1010
import com.github.continuedev.continueintellijextension.services.ContinueExtensionSettings
1111
import com.github.continuedev.continueintellijextension.services.ContinuePluginService
1212
import com.github.continuedev.continueintellijextension.services.SettingsListener
13-
import com.intellij.openapi.Disposable
13+
import com.github.continuedev.continueintellijextension.utils.toUriOrNull
1414
import com.intellij.openapi.actionSystem.KeyboardShortcut
1515
import com.intellij.openapi.application.ApplicationManager
1616
import com.intellij.openapi.application.ApplicationNamesInfo
@@ -162,7 +162,7 @@ class ContinuePluginStartupActivity : StartupActivity, DumbAware {
162162
override fun after(events: List<VFileEvent>) {
163163
// Collect all relevant URIs for deletions
164164
val deletedURIs = events.filterIsInstance<VFileDeleteEvent>()
165-
.map { event -> event.file.url }
165+
.mapNotNull { event -> event.file.toUriOrNull() }
166166

167167
// Send "files/deleted" message if there are any deletions
168168
if (deletedURIs.isNotEmpty()) {
@@ -172,7 +172,7 @@ class ContinuePluginStartupActivity : StartupActivity, DumbAware {
172172

173173
// Collect all relevant URIs for content changes
174174
val changedURIs = events.filterIsInstance<VFileContentChangeEvent>()
175-
.map { event -> event.file.url }
175+
.mapNotNull { event -> event.file.toUriOrNull() }
176176

177177
// Send "files/changed" message if there are any content changes
178178
if (changedURIs.isNotEmpty()) {
@@ -220,7 +220,7 @@ class ContinuePluginStartupActivity : StartupActivity, DumbAware {
220220
// Reload the WebView
221221
continuePluginService?.let { pluginService ->
222222
val allModulePaths = ModuleManager.getInstance(project).modules
223-
.flatMap { module -> ModuleRootManager.getInstance(module).contentRoots.map { it.url } }
223+
.flatMap { module -> ModuleRootManager.getInstance(module).contentRoots.mapNotNull { it.toUriOrNull() } }
224224

225225
val topLevelModulePaths = allModulePaths
226226
.filter { modulePath -> allModulePaths.none { it != modulePath && modulePath.startsWith(it) } }

extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/autocomplete/AcceptAutocompleteAction.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@ class AcceptAutocompleteAction : EditorAction(object : EditorActionHandler() {
2222
&& autocompleteService.pendingCompletion?.text != null
2323
return enabled
2424
}
25-
}) {}
25+
})

extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/autocomplete/AutocompleteService.kt

+52-44
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.github.continuedev.continueintellijextension.autocomplete
22

33
import com.github.continuedev.continueintellijextension.services.ContinueExtensionSettings
44
import com.github.continuedev.continueintellijextension.services.ContinuePluginService
5+
import com.github.continuedev.continueintellijextension.utils.toUriOrNull
56
import com.github.continuedev.continueintellijextension.utils.uuid
67
import com.intellij.injected.editor.VirtualFileWindow
78
import com.intellij.openapi.application.*
@@ -10,8 +11,10 @@ import com.intellij.openapi.components.ServiceManager
1011
import com.intellij.openapi.components.service
1112
import com.intellij.openapi.editor.Editor
1213
import com.intellij.openapi.editor.InlayProperties
14+
import com.intellij.openapi.editor.impl.EditorImpl
1315
import com.intellij.openapi.fileEditor.FileDocumentManager
1416
import com.intellij.openapi.project.Project
17+
import com.intellij.openapi.util.TextRange
1518
import com.intellij.openapi.wm.WindowManager
1619
import com.intellij.psi.PsiDocumentManager
1720
import com.intellij.psi.PsiElement
@@ -32,9 +35,24 @@ fun PsiElement.isInjectedText(): Boolean {
3235
return false
3336
}
3437

38+
fun Editor.addInlayElement(
39+
lines: List<String>,
40+
offset: Int,
41+
properties: InlayProperties
42+
) {
43+
if (this is EditorImpl) {
44+
if (lines[0].isNotEmpty()) {
45+
inlayModel.addInlineElement(offset, properties, ContinueInlayRenderer(listOf(lines[0])))
46+
}
47+
if (lines.size > 1) {
48+
inlayModel.addBlockElement(offset, properties, ContinueInlayRenderer(lines.drop(1)))
49+
}
50+
}
51+
}
52+
3553
@Service(Service.Level.PROJECT)
3654
class AutocompleteService(private val project: Project) {
37-
var pendingCompletion: PendingCompletion? = null;
55+
var pendingCompletion: PendingCompletion? = null
3856
private val autocompleteLookupListener = project.service<AutocompleteLookupListener>()
3957
private var widget: AutocompleteSpinnerWidget? = null
4058

@@ -66,23 +84,23 @@ class AutocompleteService(private val project: Project) {
6684

6785
// Request a completion from the core
6886
val virtualFile = FileDocumentManager.getInstance().getFile(editor.document)
87+
88+
val uri = virtualFile?.toUriOrNull() ?: return
89+
90+
val line = editor.caretModel.primaryCaret.logicalPosition.line
6991
val column = editor.caretModel.primaryCaret.logicalPosition.column
7092
val input = mapOf(
7193
"completionId" to completionId,
72-
"filepath" to virtualFile?.url,
94+
"filepath" to uri,
7395
"pos" to mapOf(
74-
"line" to editor.caretModel.primaryCaret.logicalPosition.line,
96+
"line" to line,
7597
"character" to column
7698
),
7799
"recentlyEditedFiles" to emptyList<String>(),
78100
"recentlyEditedRanges" to emptyList<String>(),
79101
"clipboardText" to ""
80102
)
81103

82-
val lineStart = editor.document.getLineStartOffset(editor.caretModel.primaryCaret.logicalPosition.line)
83-
val lineEnd = editor.document.getLineEndOffset(editor.caretModel.primaryCaret.logicalPosition.line)
84-
val lineLength = lineEnd - lineStart
85-
86104
project.service<ContinuePluginService>().coreMessenger?.request(
87105
"autocomplete/complete",
88106
input,
@@ -95,9 +113,8 @@ class AutocompleteService(private val project: Project) {
95113
val completion = completions[0].toString()
96114
val finalTextToInsert = deduplicateCompletion(editor, offset, completion)
97115

98-
if (shouldRenderCompletion(finalTextToInsert, column, lineLength, editor)) {
116+
if (shouldRenderCompletion(finalTextToInsert, offset, line, editor)) {
99117
renderCompletion(editor, offset, finalTextToInsert)
100-
pendingCompletion = PendingCompletion(editor, offset, completionId, finalTextToInsert)
101118
// Hide auto-popup
102119
// AutoPopupController.getInstance(project).cancelAllRequests()
103120
}
@@ -106,13 +123,19 @@ class AutocompleteService(private val project: Project) {
106123
)
107124
}
108125

109-
private fun shouldRenderCompletion(completion: String, column: Int, lineLength: Int, editor: Editor): Boolean {
110-
if (completion.isEmpty()) {
126+
private fun shouldRenderCompletion(completion: String, offset: Int, line: Int, editor: Editor): Boolean {
127+
if (completion.isEmpty() || runReadAction { offset != editor.caretModel.offset }) {
111128
return false
112129
}
113130

131+
if (completion.lines().size == 1) {
132+
return true
133+
}
134+
135+
val endOffset = editor.document.getLineEndOffset(line)
136+
114137
// Do not render if completion is multi-line and caret is in middle of line
115-
return !(completion.lines().size > 1 && column < lineLength)
138+
return offset <= endOffset && editor.document.getText(TextRange(offset, endOffset)).isBlank()
116139
}
117140

118141
private fun deduplicateCompletion(editor: Editor, offset: Int, completion: String): String {
@@ -126,9 +149,9 @@ class AutocompleteService(private val project: Project) {
126149

127150
val N = 10
128151
var textAfterCursor = if (caretOffset + N <= document.textLength) {
129-
document.getText(com.intellij.openapi.util.TextRange(caretOffset, caretOffset + N))
152+
document.getText(TextRange(caretOffset, caretOffset + N))
130153
} else {
131-
document.getText(com.intellij.openapi.util.TextRange(caretOffset, document.textLength))
154+
document.getText(TextRange(caretOffset, document.textLength))
132155
}
133156

134157
// Avoid truncating the completion text when the text after the cursor is blank
@@ -171,19 +194,9 @@ class AutocompleteService(private val project: Project) {
171194
properties.relatesToPrecedingText(true)
172195
properties.disableSoftWrapping(true)
173196

174-
if (completion.lines().size > 1) {
175-
editor.inlayModel.addBlockElement(
176-
offset,
177-
properties,
178-
ContinueMultilineCustomElementRenderer(editor, completion)
179-
)
180-
} else {
181-
editor.inlayModel.addInlineElement(
182-
offset,
183-
properties,
184-
ContinueCustomElementRenderer(editor, completion)
185-
)
186-
}
197+
val lines = completion.lines()
198+
pendingCompletion = pendingCompletion?.copy(text = lines.joinToString("\n"))
199+
editor.addInlayElement(lines, offset, properties)
187200

188201
// val attributes = TextAttributes().apply {
189202
// backgroundColor = JBColor.GREEN
@@ -211,7 +224,7 @@ class AutocompleteService(private val project: Project) {
211224
({})
212225
)
213226
invokeLater {
214-
clearCompletions(editor)
227+
clearCompletions(editor, completion)
215228
}
216229
}
217230

@@ -274,23 +287,14 @@ class AutocompleteService(private val project: Project) {
274287
project.service<ContinuePluginService>().coreMessenger?.request("autocomplete/cancel", null, null, ({}))
275288
}
276289

277-
fun clearCompletions(editor: Editor) {
290+
fun clearCompletions(editor: Editor, completion: PendingCompletion? = pendingCompletion) {
278291
if (isInjectedFile(editor)) return
279292

280-
if (pendingCompletion != null) {
281-
cancelCompletion(pendingCompletion!!)
282-
pendingCompletion = null
283-
}
284-
editor.inlayModel.getInlineElementsInRange(0, editor.document.textLength).forEach {
285-
if (it.renderer is ContinueCustomElementRenderer) {
286-
it.dispose()
287-
}
288-
}
289-
editor.inlayModel.getBlockElementsInRange(0, editor.document.textLength).forEach {
290-
if (it.renderer is ContinueMultilineCustomElementRenderer) {
291-
it.dispose()
292-
}
293+
if (completion != null) {
294+
cancelCompletion(completion)
295+
if (completion.completionId == pendingCompletion?.completionId) pendingCompletion = null
293296
}
297+
disposeInlayRenderer(editor)
294298
}
295299

296300
private fun isInjectedFile(editor: Editor): Boolean {
@@ -306,13 +310,17 @@ class AutocompleteService(private val project: Project) {
306310
fun hideCompletions(editor: Editor) {
307311
if (isInjectedFile(editor)) return
308312

313+
disposeInlayRenderer(editor)
314+
}
315+
316+
private fun disposeInlayRenderer(editor: Editor) {
309317
editor.inlayModel.getInlineElementsInRange(0, editor.document.textLength).forEach {
310-
if (it.renderer is ContinueCustomElementRenderer) {
318+
if (it.renderer is ContinueInlayRenderer) {
311319
it.dispose()
312320
}
313321
}
314322
editor.inlayModel.getBlockElementsInRange(0, editor.document.textLength).forEach {
315-
if (it.renderer is ContinueMultilineCustomElementRenderer) {
323+
if (it.renderer is ContinueInlayRenderer) {
316324
it.dispose()
317325
}
318326
}

extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/autocomplete/ContinueCustomElementRenderer.kt

-45
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.github.continuedev.continueintellijextension.autocomplete
2+
3+
import com.intellij.openapi.editor.Editor
4+
import com.intellij.openapi.editor.EditorCustomElementRenderer
5+
import com.intellij.openapi.editor.Inlay
6+
import com.intellij.openapi.editor.colors.EditorFontType
7+
import com.intellij.openapi.editor.impl.EditorImpl
8+
import com.intellij.openapi.editor.markup.TextAttributes
9+
import com.intellij.ui.JBColor
10+
import com.intellij.util.ui.UIUtil
11+
import java.awt.Font
12+
import java.awt.Graphics
13+
import java.awt.Rectangle
14+
15+
/**
16+
* The `ContinueInlayRenderer` class is responsible for rendering custom inlay elements within an editor.
17+
* It implements the [EditorCustomElementRenderer] interface to provide custom rendering logic for inlays.
18+
*
19+
* This renderer is designed to display a list of text lines (`lines`) within the editor, calculating the
20+
* necessary width and height based on the content and rendering each line with appropriate font and color.
21+
*
22+
* @author lk
23+
*/
24+
class ContinueInlayRenderer(val lines: List<String>) : EditorCustomElementRenderer {
25+
override fun calcWidthInPixels(inlay: Inlay<*>): Int {
26+
var maxLen = 0;
27+
for (line in lines) {
28+
val len = (inlay.editor as EditorImpl).getFontMetrics(Font.PLAIN).stringWidth(line)
29+
if (len > maxLen) {
30+
maxLen = len
31+
}
32+
}
33+
return maxLen
34+
}
35+
36+
override fun calcHeightInPixels(inlay: Inlay<*>): Int {
37+
return (inlay.editor as EditorImpl).lineHeight * lines.size
38+
}
39+
40+
private fun font(editor: Editor): Font {
41+
val editorFont = editor.colorsScheme.getFont(EditorFontType.PLAIN)
42+
return UIUtil.getFontWithFallbackIfNeeded(editorFont, lines.joinToString("\n"))
43+
.deriveFont(editor.colorsScheme.editorFontSize)
44+
}
45+
46+
override fun paint(inlay: Inlay<*>, g: Graphics, targetRegion: Rectangle, textAttributes: TextAttributes) {
47+
val editor = inlay.editor
48+
g.color = JBColor.GRAY
49+
g.font = font(editor)
50+
var additionalYOffset = 0
51+
val ascent = editor.ascent
52+
val lineHeight = editor.lineHeight
53+
for (line in lines) {
54+
g.drawString(line, targetRegion.x, targetRegion.y + ascent + additionalYOffset)
55+
additionalYOffset += lineHeight
56+
}
57+
}
58+
}

0 commit comments

Comments
 (0)