diff --git a/platform/inline-completion/shared/intellij.platform.inline.completion.iml b/platform/inline-completion/shared/intellij.platform.inline.completion.iml
index 807a701820d8..35f4c084abed 100644
--- a/platform/inline-completion/shared/intellij.platform.inline.completion.iml
+++ b/platform/inline-completion/shared/intellij.platform.inline.completion.iml
@@ -14,5 +14,7 @@
+
+
\ No newline at end of file
diff --git a/platform/inline-completion/shared/resources/intellij.platform.inline.completion.xml b/platform/inline-completion/shared/resources/intellij.platform.inline.completion.xml
index fbbfdd5df949..a424cd4af7b9 100644
--- a/platform/inline-completion/shared/resources/intellij.platform.inline.completion.xml
+++ b/platform/inline-completion/shared/resources/intellij.platform.inline.completion.xml
@@ -4,4 +4,12 @@
description="Use Rhizome DB for inline completion implementation in RemDev"
defaultValue="false"/>
+
+
+
+
\ No newline at end of file
diff --git a/platform/inline-completion/shared/resources/messages/InlineCompletionBundle.properties b/platform/inline-completion/shared/resources/messages/InlineCompletionBundle.properties
new file mode 100644
index 000000000000..c3ca983f4706
--- /dev/null
+++ b/platform/inline-completion/shared/resources/messages/InlineCompletionBundle.properties
@@ -0,0 +1,4 @@
+inline.completion.shortcut.hint.insert.text=to complete
+inline.completion.shortcut.hint.insert.word.text=to complete word
+inline.completion.shortcut.hint.insert.line.text=to complete line
+inline.completion.shortcut.choose.shortcut.hint.text=Choose key for completion:
\ No newline at end of file
diff --git a/platform/inline-completion/shared/src/MessageBundle.kt b/platform/inline-completion/shared/src/MessageBundle.kt
new file mode 100644
index 000000000000..71db389b4764
--- /dev/null
+++ b/platform/inline-completion/shared/src/MessageBundle.kt
@@ -0,0 +1,17 @@
+// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.codeInsight.inline.completion
+
+import com.intellij.DynamicBundle
+import org.jetbrains.annotations.Nls
+import org.jetbrains.annotations.NotNull
+import org.jetbrains.annotations.PropertyKey
+
+internal object MessageBundle {
+ private const val BUNDLE = "messages.InlineCompletionBundle"
+ private val INSTANCE = DynamicBundle(MessageBundle::class.java, BUNDLE)
+
+ @JvmStatic
+ fun message(@NotNull @PropertyKey(resourceBundle = BUNDLE) key: String, @NotNull vararg params: Any): @Nls String {
+ return INSTANCE.getMessage(key, *params)
+ }
+}
\ No newline at end of file
diff --git a/platform/inline-completion/shared/src/shortcut/InlineCompletionChooseShortcutElement.kt b/platform/inline-completion/shared/src/shortcut/InlineCompletionChooseShortcutElement.kt
new file mode 100644
index 000000000000..43b53c44cec4
--- /dev/null
+++ b/platform/inline-completion/shared/src/shortcut/InlineCompletionChooseShortcutElement.kt
@@ -0,0 +1,272 @@
+// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.codeInsight.inline.completion.shortcut
+
+import com.intellij.codeInsight.hints.presentation.InputHandler
+import com.intellij.codeInsight.inline.completion.MessageBundle
+import com.intellij.codeInsight.inline.completion.elements.InlineCompletionElement
+import com.intellij.codeInsight.inline.completion.tooltip.ChangeToCustomInlineCompletionAction
+import com.intellij.codeInsight.inline.completion.tooltip.InplaceChangeInlineCompletionShortcutAction
+import com.intellij.codeInsight.inline.hint.InlineShortcutHintRendererBase
+import com.intellij.ide.IdeBundle
+import com.intellij.openapi.actionSystem.*
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.editor.Editor
+import com.intellij.openapi.editor.Inlay
+import com.intellij.openapi.editor.impl.EditorImpl
+import com.intellij.openapi.editor.markup.TextAttributes
+import com.intellij.openapi.observable.util.whenDisposed
+import com.intellij.openapi.ui.popup.JBPopupFactory
+import com.intellij.openapi.util.Disposer
+import com.intellij.openapi.util.NlsActions
+import com.intellij.ui.awt.RelativePoint
+import com.intellij.util.ui.JBUI
+import com.intellij.util.ui.UIUtil
+import org.jetbrains.annotations.ApiStatus
+import java.awt.Cursor
+import java.awt.Graphics
+import java.awt.Point
+import java.awt.Rectangle
+import java.awt.event.MouseEvent
+
+@ApiStatus.Internal
+class InlineCompletionChooseShortcutElement(val lineNumber: Int) : InlineCompletionElement {
+ override val text: String = ""
+
+ init {
+ InlineCompletionShortcutHintListener.choiceShown()
+ }
+
+ override fun toPresentable(): InlineCompletionElement.Presentable {
+ return Presentable(this)
+ }
+
+ class Presentable(override val element: InlineCompletionChooseShortcutElement) : InlineCompletionElement.Presentable {
+ private var shortcutInlay: Inlay? = null
+ private var textInlay: Inlay? = null
+ private var offset: Int? = null
+
+ override fun isVisible(): Boolean {
+ return getBounds() != null
+ }
+
+ override fun getBounds(): Rectangle? {
+ if (!choiceShouldRender()) {
+ return null
+ }
+ val shortcutBounds = shortcutInlay?.bounds ?: return null
+ val suffixBounds = textInlay?.bounds ?: return null
+ return shortcutBounds.union(suffixBounds)
+ }
+
+ override fun startOffset(): Int? = offset
+
+ override fun endOffset(): Int? = offset
+
+ override fun dispose() {
+ shortcutInlay?.let(Disposer::dispose)
+ textInlay?.let(Disposer::dispose)
+ shortcutInlay = null
+ textInlay = null
+ offset = null
+ }
+
+ override fun render(editor: Editor, offset: Int) {
+ InlineCompletionShortcutChangeListener.whenInsertShortcutChanged(disposable = this) {
+ shortcutInlay?.update()
+ textInlay?.update()
+ }
+
+ if (editor !is EditorImpl || !InlineShortcutHintRendererBase.isAvailableForLine(editor, element.lineNumber)) {
+ return
+ }
+ try {
+ val caretOffset = editor.caretModel.offset
+ val hintState = HintState(false)
+ val textBefore = MessageBundle.message("inline.completion.shortcut.choose.shortcut.hint.text")
+ val textRenderer = TextRenderer(textBefore, hintState, element.lineNumber)
+ val shortcutRenderer = ShortcutRenderer("Tab " + '\u25BE', hintState, element.lineNumber)
+ textInlay = editor.inlayModel.addAfterLineEndElement(caretOffset, true, textRenderer).apply {
+ renderer.initialize(this)
+ }
+ shortcutInlay = editor.inlayModel.addAfterLineEndElement(caretOffset, true, shortcutRenderer).apply {
+ renderer.initialize(this)
+ }
+ this.offset = offset
+ }
+ catch (e: Exception) {
+ LOG.error("Could not render ML completion in-editor choice hint.", e)
+ }
+ }
+
+ companion object {
+ private val LOG = logger()
+ }
+ }
+}
+
+private class HintState(var isHovered: Boolean) {
+ private val updates = mutableListOf<() -> Unit>()
+
+ fun whenUpdated(block: () -> Unit) {
+ updates += block
+ }
+
+ fun update() {
+ updates.forEach { it() }
+ }
+}
+
+private abstract class RendererBase(
+ text: String?,
+ val hintState: HintState,
+ private val lineNumber: Int
+) : InlineCompletionShortcutHintRendererBase(text), InputHandler {
+ private lateinit var myInlay: Inlay<*>
+
+ override fun getTextAttributes(editor: Editor): TextAttributes? {
+ return super.getTextAttributes(editor)?.updateIfHovering()
+ }
+
+ override fun calcWidthInPixels(inlay: Inlay<*>): Int {
+ return if (!choiceShouldRender()) 1 else super.calcWidthInPixels(inlay)
+ }
+
+ fun initialize(inlay: Inlay<*>) {
+ check(!::myInlay.isInitialized)
+ myInlay = inlay
+ inlay.whenDisposed {
+ updateState(false)
+ }
+ hintState.whenUpdated {
+ inlay.update()
+ }
+ }
+
+ fun TextAttributes.updateIfHovering(): TextAttributes {
+ return if (hintState.isHovered) {
+ clone().apply {
+ foregroundColor = JBUI.CurrentTheme.Link.Foreground.ENABLED
+ }
+ }
+ else this
+ }
+
+ override fun isEnabledAdditional(editor: Editor): Boolean = choiceShouldRender()
+
+ override fun mousePressed(event: MouseEvent, translated: Point) {
+ if (choiceShouldRender()) {
+ event.consume()
+ }
+ }
+
+ override fun mouseReleased(event: MouseEvent, translated: Point) {
+ if (choiceShouldRender()) {
+ showChooseShortcutPopup(myInlay.editor, guessPopupPoint(event), lineNumber)
+ }
+ }
+
+ override fun mouseMoved(event: MouseEvent, translated: Point) {
+ if (choiceShouldRender()) {
+ updateState(true)
+ }
+ }
+
+ override fun mouseExited() {
+ if (choiceShouldRender()) {
+ updateState(false)
+ }
+ }
+
+ private fun updateState(isHovered: Boolean) {
+ hintState.isHovered = isHovered
+ if (myInlay.isValid) {
+ updateCursor(isHovered)
+ hintState.update()
+ }
+ }
+
+ private fun updateCursor(isHovered: Boolean) {
+ val cursor = when (isHovered) {
+ true -> Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
+ false -> Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)
+ }
+ val contentComponent = myInlay.editor.contentComponent
+ if (contentComponent.cursor != cursor) {
+ UIUtil.setCursor(contentComponent, cursor)
+ }
+ }
+
+ private fun guessPopupPoint(event: MouseEvent): RelativePoint {
+ val shortcutInlay = myInlay.editor.inlayModel.getAfterLineEndElementsForLogicalLine(lineNumber).firstOrNull {
+ it.renderer is ShortcutRenderer
+ }
+ val point = shortcutInlay?.bounds?.let { Point(it.x, it.y) } ?: event.point
+ return RelativePoint(event.component, Point(point.x, point.y + myInlay.editor.lineHeight))
+ }
+}
+
+private class ShortcutRenderer(
+ text: String?,
+ hoveringState: HintState,
+ lineNumber: Int
+) : RendererBase(text, hoveringState, lineNumber), InputHandler {
+
+ override fun paintIfEnabled(inlay: Inlay<*>, g: Graphics, r: Rectangle, textAttributes: TextAttributes) {
+ paintHint(inlay, g, r, textAttributes.updateIfHovering().clearEffects())
+ }
+}
+
+private class TextRenderer(
+ text: String?,
+ hoveringState: HintState,
+ lineNumber: Int
+) : RendererBase(text, hoveringState, lineNumber), InputHandler {
+
+ override fun paintIfEnabled(inlay: Inlay<*>, g: Graphics, r: Rectangle, textAttributes: TextAttributes) {
+ paintLabel(g, inlay.editor as EditorImpl, r, text, textAttributes) { it.updateIfHovering() }
+ }
+}
+
+private fun choiceShouldRender(): Boolean {
+ return InlineCompletionShortcutHintState.getState() == InlineCompletionShortcutHintState.SHOW_CHOICE
+}
+
+private fun showChooseShortcutPopup(editor: Editor, relativePoint: RelativePoint, lineNumber: Int) {
+ val predefinedShortcuts = listOf(
+ "Tab" to KeyboardShortcut.fromString("TAB"),
+ "→" to KeyboardShortcut.fromString("RIGHT"),
+ "Enter" to KeyboardShortcut.fromString("ENTER"),
+ "Shift →" to KeyboardShortcut.fromString("shift pressed RIGHT")
+ ).map { (name, shortcut) ->
+ ChangeShortcutAction(name, shortcut, editor, lineNumber)
+ }
+ val defaultGroup = DefaultActionGroup().apply {
+ addAll(predefinedShortcuts)
+ add(ChangeToCustomInlineCompletionAction())
+ }
+ val popup = JBPopupFactory.getInstance().createActionGroupPopup(
+ IdeBundle.message("inline.completion.tooltip.shortcuts.header"),
+ defaultGroup,
+ DataContext.EMPTY_CONTEXT,
+ true,
+ null,
+ 10
+ )
+ popup.show(relativePoint)
+}
+
+private class ChangeShortcutAction(
+ @NlsActions.ActionText text: String,
+ shortcut: Shortcut,
+ private val editor: Editor,
+ private val lineNumber: Int
+) : InplaceChangeInlineCompletionShortcutAction(text, shortcut) {
+
+ override fun actionPerformed(e: AnActionEvent) {
+ super.actionPerformed(e)
+ InlineCompletionShortcutHintListener.choiceMade()
+ val inlaysAfterLine = editor.inlayModel.getAfterLineEndElementsForLogicalLine(lineNumber)
+ val relatedInlays = inlaysAfterLine.filter { it.renderer is InlineCompletionShortcutHintRendererBase }
+ relatedInlays.forEach { it.update() }
+ }
+}
diff --git a/platform/inline-completion/shared/src/shortcut/InlineCompletionShortcutChangeListener.kt b/platform/inline-completion/shared/src/shortcut/InlineCompletionShortcutChangeListener.kt
new file mode 100644
index 000000000000..651095fa1e6e
--- /dev/null
+++ b/platform/inline-completion/shared/src/shortcut/InlineCompletionShortcutChangeListener.kt
@@ -0,0 +1,32 @@
+// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.codeInsight.inline.completion.shortcut
+
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.actionSystem.IdeActions
+import com.intellij.openapi.keymap.Keymap
+import com.intellij.openapi.keymap.KeymapManagerListener
+import com.intellij.util.application
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.annotations.NonNls
+
+@ApiStatus.Internal
+abstract class InlineCompletionShortcutChangeListener : KeymapManagerListener {
+
+ protected abstract fun insertShortcutChanged()
+
+ final override fun shortcutsChanged(keymap: Keymap, actionIds: @NonNls MutableCollection, fromSettings: Boolean) {
+ val expectedActionId = IdeActions.ACTION_INSERT_INLINE_COMPLETION
+ if (actionIds.contains(expectedActionId) && keymap.getShortcuts(expectedActionId).isNotEmpty()) {
+ insertShortcutChanged()
+ }
+ }
+
+ companion object {
+ fun whenInsertShortcutChanged(disposable: Disposable, handler: () -> Unit) {
+ val listener = object : InlineCompletionShortcutChangeListener() {
+ override fun insertShortcutChanged() = handler()
+ }
+ application.messageBus.connect(disposable).subscribe(KeymapManagerListener.TOPIC, listener)
+ }
+ }
+}
diff --git a/platform/inline-completion/shared/src/shortcut/InlineCompletionShortcutHintElement.kt b/platform/inline-completion/shared/src/shortcut/InlineCompletionShortcutHintElement.kt
new file mode 100644
index 000000000000..2dec7e64af0a
--- /dev/null
+++ b/platform/inline-completion/shared/src/shortcut/InlineCompletionShortcutHintElement.kt
@@ -0,0 +1,186 @@
+// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.codeInsight.inline.completion.shortcut
+
+import com.intellij.codeInsight.inline.completion.MessageBundle
+import com.intellij.codeInsight.inline.completion.elements.InlineCompletionElement
+import com.intellij.codeInsight.inline.completion.shortcut.InlineCompletionShortcutHint.entries
+import com.intellij.codeInsight.inline.hint.InlineShortcutHintRendererBase
+import com.intellij.openapi.actionSystem.IdeActions
+import com.intellij.openapi.actionSystem.Shortcut
+import com.intellij.openapi.diagnostic.thisLogger
+import com.intellij.openapi.editor.Editor
+import com.intellij.openapi.editor.Inlay
+import com.intellij.openapi.editor.impl.EditorImpl
+import com.intellij.openapi.editor.markup.TextAttributes
+import com.intellij.openapi.keymap.KeymapUtil
+import com.intellij.openapi.util.Disposer
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.annotations.Nls
+import java.awt.Graphics
+import java.awt.Rectangle
+import kotlin.random.Random
+
+@ApiStatus.Internal
+class InlineCompletionShortcutHintElement(val lineNumber: Int, val isMultiline: Boolean) : InlineCompletionElement {
+ override val text: String = ""
+
+ private val hint = InlineCompletionShortcutHint.random(isMultiline)
+
+ override fun toPresentable(): InlineCompletionElement.Presentable {
+ return Presentable(this, hint)
+ }
+
+ class Presentable(
+ override val element: InlineCompletionShortcutHintElement,
+ private val hint: InlineCompletionShortcutHint
+ ) : InlineCompletionElement.Presentable {
+ private var shortcutInlay: Inlay? = null
+ private var suffixInlay: Inlay? = null
+ private var offset: Int? = null
+ private var currentShortcut = getShortcutRepresentation()
+
+ init {
+ InlineCompletionShortcutChangeListener.whenInsertShortcutChanged(disposable = this) {
+ rerender()
+ }
+ }
+
+ override fun isVisible(): Boolean {
+ return getBounds() != null
+ }
+
+ override fun getBounds(): Rectangle? {
+ if (!hintShouldRender()) {
+ return null
+ }
+ val shortcutBounds = shortcutInlay?.bounds ?: return null
+ val suffixBounds = suffixInlay?.bounds ?: return null
+ return shortcutBounds.union(suffixBounds)
+ }
+
+ override fun startOffset(): Int? = offset
+
+ override fun endOffset(): Int? = offset
+
+ override fun dispose() {
+ shortcutInlay?.let(Disposer::dispose)
+ suffixInlay?.let(Disposer::dispose)
+ shortcutInlay = null
+ suffixInlay = null
+ offset = null
+ }
+
+ override fun render(editor: Editor, offset: Int) {
+ if (editor !is EditorImpl || currentShortcut == null || !InlineShortcutHintRendererBase.isAvailableForLine(editor, element.lineNumber)) {
+ return
+ }
+ try {
+ val caretOffset = editor.caretModel.offset
+ shortcutInlay = editor.inlayModel.addAfterLineEndElement(caretOffset, true, getShortcutRenderer())
+ suffixInlay = editor.inlayModel.addAfterLineEndElement(caretOffset, true, getSuffixRenderer(editor))
+ this.offset = offset
+ }
+ catch (e: Exception) {
+ LOG.error("Could not render Full Line in-editor hint.", e)
+ }
+ }
+
+ private fun getSuffixRenderer(editor: EditorImpl): InlineCompletionShortcutHintRendererBase {
+ return object : InlineCompletionShortcutHintRendererBase(hint.suffixText) {
+
+ // We need to check it here to be able to re-draw if a user changes the setting during a session
+ override fun isEnabledAdditional(editor: Editor): Boolean = hintShouldRender()
+
+ override fun paintIfEnabled(inlay: Inlay<*>, g: Graphics, r: Rectangle, textAttributes: TextAttributes) {
+ paintLabel(g, editor, r, text, textAttributes.clearEffects())
+ }
+ }
+ }
+
+ private fun getShortcutRenderer(): InlineCompletionShortcutHintRendererBase {
+ return object : InlineCompletionShortcutHintRendererBase(currentShortcut) {
+
+ // We need to check it here to be able to re-draw if a user changes the setting during a session
+ override fun isEnabledAdditional(editor: Editor): Boolean = hintShouldRender()
+
+ override fun paintIfEnabled(inlay: Inlay<*>, g: Graphics, r: Rectangle, textAttributes: TextAttributes) {
+ paintHint(inlay, g, r, textAttributes.clearEffects())
+ }
+ }
+ }
+
+ private fun rerender() {
+ currentShortcut = getShortcutRepresentation()
+ shortcutInlay?.renderer?.text = currentShortcut
+ shortcutInlay?.update()
+ suffixInlay?.update()
+ }
+
+ private fun getShortcutRepresentation(): String? {
+ val shortcut = KeymapUtil.getPrimaryShortcut(hint.actionId)
+ return shortcut?.asString()
+ }
+
+ private fun Shortcut.asString(): String {
+ return when (toString()) {
+ "[pressed TAB]" -> "Tab"
+ "[pressed ENTER]" -> "Enter"
+ "[shift pressed RIGHT]" -> "Shift →"
+ else -> KeymapUtil.getShortcutText(this)
+ }
+ }
+
+ private fun hintShouldRender(): Boolean {
+ return currentShortcut != null && InlineCompletionShortcutHintState.getState() == InlineCompletionShortcutHintState.SHOW_HINT
+ }
+
+ companion object {
+ private val LOG = thisLogger()
+ }
+ }
+}
+
+@ApiStatus.Internal
+enum class InlineCompletionShortcutHint {
+ INSERT {
+ override val actionId: String
+ get() = IdeActions.ACTION_INSERT_INLINE_COMPLETION
+ override val suffixText: String
+ get() = MessageBundle.message("inline.completion.shortcut.hint.insert.text")
+ override val priority: Int
+ get() = 4
+ },
+ INSERT_WORD {
+ override val actionId: String
+ get() = IdeActions.ACTION_INSERT_INLINE_COMPLETION_WORD
+ override val suffixText: String
+ get() = MessageBundle.message("inline.completion.shortcut.hint.insert.word.text")
+ override val priority: Int
+ get() = 1
+ },
+ INSERT_LINE {
+ override val actionId: String
+ get() = IdeActions.ACTION_INSERT_INLINE_COMPLETION_LINE
+ override val suffixText: String
+ get() = MessageBundle.message("inline.completion.shortcut.hint.insert.line.text")
+ override val priority: Int
+ get() = 3
+ };
+
+ abstract val actionId: String
+
+ abstract val suffixText: @Nls String
+
+ protected abstract val priority: Int
+
+ companion object {
+ fun random(isMultiline: Boolean): InlineCompletionShortcutHint {
+ val entries = if (isMultiline) entries else entries.filter { it !== INSERT_LINE }
+ var randomPoint = Random.nextInt(entries.sumOf { it.priority }) + 1
+ return entries.firstOrNull {
+ randomPoint -= it.priority
+ randomPoint <= 0
+ } ?: entries.last()
+ }
+ }
+}
diff --git a/platform/inline-completion/shared/src/shortcut/InlineCompletionShortcutHintListener.kt b/platform/inline-completion/shared/src/shortcut/InlineCompletionShortcutHintListener.kt
new file mode 100644
index 000000000000..868bbf4793ac
--- /dev/null
+++ b/platform/inline-completion/shared/src/shortcut/InlineCompletionShortcutHintListener.kt
@@ -0,0 +1,47 @@
+// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.codeInsight.inline.completion.shortcut
+
+import com.intellij.openapi.editor.Editor
+import com.intellij.openapi.extensions.ExtensionPointName
+import org.jetbrains.annotations.ApiStatus
+
+@ApiStatus.Internal
+@ApiStatus.NonExtendable
+interface InlineCompletionShortcutHintListener {
+
+ fun choiceShown()
+
+ fun choiceMade()
+
+ fun shouldShowChoice(): Boolean
+
+ fun shouldHideHint(editor: Editor): Boolean
+
+ fun isHintDisabled(): Boolean
+
+ companion object {
+ private val EP_NAME = ExtensionPointName.create(
+ "com.intellij.inline.completion.shortcutHintListener"
+ )
+
+ fun choiceShown() {
+ EP_NAME.forEachExtensionSafe { it.choiceShown() }
+ }
+
+ fun choiceMade() {
+ EP_NAME.forEachExtensionSafe { it.choiceMade() }
+ }
+
+ fun shouldShowChoice(): Boolean {
+ return EP_NAME.extensionList.any { it.shouldShowChoice() }
+ }
+
+ fun shouldHideHint(editor: Editor): Boolean {
+ return EP_NAME.extensionList.any { it.shouldHideHint(editor) }
+ }
+
+ fun isHintDisabled(): Boolean {
+ return EP_NAME.extensionList.any { it.isHintDisabled() }
+ }
+ }
+}
diff --git a/platform/inline-completion/shared/src/shortcut/InlineCompletionShortcutHintRendererBase.kt b/platform/inline-completion/shared/src/shortcut/InlineCompletionShortcutHintRendererBase.kt
new file mode 100644
index 000000000000..dc2e6e8fa307
--- /dev/null
+++ b/platform/inline-completion/shared/src/shortcut/InlineCompletionShortcutHintRendererBase.kt
@@ -0,0 +1,16 @@
+// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.codeInsight.inline.completion.shortcut
+
+import com.intellij.codeInsight.inline.hint.InlineShortcutHintRendererBase
+import com.intellij.openapi.editor.Editor
+import org.jetbrains.annotations.ApiStatus
+
+@ApiStatus.Internal
+abstract class InlineCompletionShortcutHintRendererBase(text: String?) : InlineShortcutHintRendererBase(text) {
+
+ protected abstract fun isEnabledAdditional(editor: Editor): Boolean
+
+ final override fun isEnabled(editor: Editor): Boolean {
+ return isEnabledAdditional(editor) && !InlineCompletionShortcutHintListener.shouldHideHint(editor)
+ }
+}
diff --git a/platform/inline-completion/shared/src/shortcut/InlineCompletionShortcutHintState.kt b/platform/inline-completion/shared/src/shortcut/InlineCompletionShortcutHintState.kt
new file mode 100644
index 000000000000..51d28f508e69
--- /dev/null
+++ b/platform/inline-completion/shared/src/shortcut/InlineCompletionShortcutHintState.kt
@@ -0,0 +1,33 @@
+// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.codeInsight.inline.completion.shortcut
+
+import com.intellij.openapi.actionSystem.IdeActions
+import com.intellij.openapi.actionSystem.Shortcut
+import com.intellij.openapi.keymap.KeymapUtil
+import com.intellij.util.application
+import org.jetbrains.annotations.ApiStatus
+
+@ApiStatus.Internal
+enum class InlineCompletionShortcutHintState {
+ SHOW_HINT,
+ SHOW_CHOICE,
+ SHOW_NOTHING;
+
+ companion object {
+
+ fun getState(): InlineCompletionShortcutHintState {
+ val primaryShortcut = KeymapUtil.getPrimaryShortcut(IdeActions.ACTION_INSERT_INLINE_COMPLETION)
+ if (primaryShortcut?.isTab() == false) {
+ InlineCompletionShortcutHintListener.choiceMade()
+ }
+ if (primaryShortcut == null || InlineCompletionShortcutHintListener.isHintDisabled()) {
+ return SHOW_NOTHING
+ }
+ return if (InlineCompletionShortcutHintListener.shouldShowChoice() && !application.isUnitTestMode) SHOW_CHOICE else SHOW_HINT
+ }
+
+ private fun Shortcut.isTab(): Boolean {
+ return toString().lowercase() == "[pressed tab]"
+ }
+ }
+}
diff --git a/platform/platform-impl/src/com/intellij/codeInsight/inline/hint/InlineShortcutHintRendererBase.kt b/platform/platform-impl/src/com/intellij/codeInsight/inline/hint/InlineShortcutHintRendererBase.kt
index 578311035fe7..91d334f7d657 100644
--- a/platform/platform-impl/src/com/intellij/codeInsight/inline/hint/InlineShortcutHintRendererBase.kt
+++ b/platform/platform-impl/src/com/intellij/codeInsight/inline/hint/InlineShortcutHintRendererBase.kt
@@ -3,6 +3,9 @@ package com.intellij.codeInsight.inline.hint
import com.intellij.codeInsight.daemon.impl.HintRenderer
import com.intellij.codeInsight.inline.completion.InlineCompletionFontUtils
+import com.intellij.codeWithMe.ClientId
+import com.intellij.codeWithMe.ClientId.Companion.isLocal
+import com.intellij.idea.AppMode
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.impl.EditorImpl
@@ -72,6 +75,9 @@ abstract class InlineShortcutHintRendererBase(text: String?) : HintRenderer(text
companion object {
@JvmStatic
fun isAvailableForLine(editor: Editor, lineNumber: Int): Boolean {
+ if (!ClientId.currentOrNull.isLocal || AppMode.isRemoteDevHost()) {
+ return false
+ }
// Debugger may suggest something on the right, so inlays will mix up
val inlaysAfterLine = editor.inlayModel.getAfterLineEndElementsForLogicalLine(lineNumber)
return inlaysAfterLine.all { it.renderer is InlineShortcutHintRendererBase }