LLM-18992 [ai-next-edit] request NES when inline completion is triggered by shortcut

(cherry picked from commit 165ad21d571bb6a479179222fd8cebd85f18a0ee)

IJ-CR-183016

GitOrigin-RevId: 61d3bd8d01ed3cbedb7adb4ceb32731d01a73e50
This commit is contained in:
Kirill.Karnaukhov
2025-11-21 13:50:35 +00:00
committed by intellij-monorepo-bot
parent 538c752411
commit 3140d68eb6
12 changed files with 180 additions and 47 deletions
@@ -20,7 +20,6 @@ internal class InlineCompletionHandlerImpl(
) : InlineCompletionHandler(scope, editor, parentDisposable) {
init {
addEventListener(InlineCompletionNoSuggestionsListener(editor))
InlineCompletionOnboardingListener.createIfOnboarding(editor)?.let(::addEventListener)
}
@@ -1,20 +0,0 @@
// 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.codeInsight.hint.HintManager
import com.intellij.codeInsight.inline.completion.logs.InlineCompletionUsageTracker
import com.intellij.codeInsight.inline.completion.session.InlineCompletionSession
import com.intellij.lang.LangBundle
import com.intellij.openapi.editor.Editor
internal class InlineCompletionNoSuggestionsListener(private val editor: Editor) : InlineCompletionEventAdapter {
override fun onHide(event: InlineCompletionEventType.Hide) {
if (event.finishType == InlineCompletionUsageTracker.ShownEvents.FinishType.EMPTY) {
val session = InlineCompletionSession.getOrNull(editor)
if (session != null && session.request.event is InlineCompletionEvent.DirectCall) {
HintManager.getInstance().showInformationHint(editor, LangBundle.message("completion.no.suggestions"), HintManager.ABOVE)
}
}
}
}
@@ -126,19 +126,6 @@ class BackspaceDeleteInlineCompletionHandler(private val originalHandler: Editor
}
}
@ApiStatus.Internal
class CallInlineCompletionAction : EditorAction(CallInlineCompletionHandler()), HintManagerImpl.ActionToIgnore {
class CallInlineCompletionHandler : EditorWriteActionHandler() {
override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
val curCaret = caret ?: editor.caretModel.currentCaret
val listener = InlineCompletion.getHandlerOrNull(editor) ?: return
listener.invoke(InlineCompletionEvent.DirectCall(editor, curCaret, dataContext))
}
}
}
@ApiStatus.Experimental
@ApiStatus.Internal
class InsertInlineCompletionWordAction : EditorAction(Handler()), HintManagerImpl.ActionToIgnore {
@@ -1,6 +1,7 @@
// 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.codeInsight.inline.completion.action.CallInlineCompletionAction
import com.intellij.codeInsight.inline.completion.session.InlineCompletionContext
import com.intellij.openapi.actionSystem.ActionPromoter
import com.intellij.openapi.actionSystem.AnAction
@@ -590,9 +590,7 @@ abstract class InlineCompletionHandler @ApiStatus.Internal constructor(
// -----------------------------------
@TestOnly
suspend fun awaitExecution() {
ThreadingAssertions.assertEventDispatchThread()
executor.awaitActiveRequest()
}
@@ -0,0 +1,51 @@
// Copyright 2000-2025 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.action
import com.intellij.codeInsight.hint.HintManagerImpl
import com.intellij.codeInsight.inline.completion.InlineCompletion
import com.intellij.codeInsight.inline.completion.InlineCompletionEvent
import com.intellij.codeInsight.inline.edit.NoInlineEditShownNotifier
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.EditorAction
import com.intellij.openapi.editor.actionSystem.EditorActionHandler
import com.intellij.openapi.editor.actionSystem.EditorWriteActionHandler
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
class CallInlineCompletionAction : EditorAction(CallInlineCompletionHandler()), HintManagerImpl.ActionToIgnore {
class CallInlineCompletionHandler : EditorWriteActionHandler() {
override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
val curCaret = caret ?: editor.caretModel.currentCaret
val handler = InlineCompletion.getHandlerOrNull(editor) ?: return
handler.invokeEvent(InlineCompletionEvent.DirectCall(editor, curCaret, dataContext))
}
override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean {
return InlineCompletion.getHandlerOrNull(editor) != null
}
}
internal class ShowNoSuggestionsHintIfNeededHandler(private val originalHandler: EditorActionHandler) : EditorWriteActionHandler() {
override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) {
if (originalHandler.isEnabled(editor, caret, dataContext)) {
originalHandler.execute(editor, caret, dataContext)
}
if (InlineCompletion.getHandlerOrNull(editor) != null) {
val project = dataContext?.getData(CommonDataKeys.PROJECT) ?: editor.project
project?.let { project ->
NoInlineEditShownNotifier.getInstance(project).notifyNoSuggestionIfNothingIsShown(editor)
}
}
}
override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean {
return InlineCompletion.getHandlerOrNull(editor) != null || originalHandler.isEnabled(editor, caret, dataContext)
}
}
}
@@ -0,0 +1,47 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.inline.edit
import com.intellij.codeInsight.inline.completion.InlineCompletion
import com.intellij.codeInsight.inline.completion.session.InlineCompletionSession
import com.intellij.openapi.application.EDT
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.project.Project
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
/**
* Provides a way to wait for all possible inline edit providers: inline completion and Next Edit.
*
* The method returns whether a particular inline edit source shows something or no edits were proposed.
*/
@ApiStatus.Internal
interface InlineEditAwaiter {
suspend fun awaitInlineEdit(project: Project, editor: Editor): Result
enum class Result {
SuggestionProvided,
NothingProvided,
}
companion object {
private val EP_NAME = ExtensionPointName.create<InlineEditAwaiter>("com.intellij.inline.edit.awaiter")
suspend fun awaitAllInlineEdits(project: Project, editor: Editor): Result {
val isAnythingProvided = EP_NAME.extensionList.any { it.awaitInlineEdit(project, editor) == Result.SuggestionProvided }
return if (isAnythingProvided) Result.SuggestionProvided else Result.NothingProvided
}
}
}
internal class InlineCompletionAwaiter : InlineEditAwaiter {
override suspend fun awaitInlineEdit(project: Project, editor: Editor): InlineEditAwaiter.Result {
InlineCompletion.getHandlerOrNull(editor)?.awaitExecution()
return withContext(Dispatchers.EDT) {
val isShowing = InlineCompletionSession.getOrNull(editor)?.context?.isCurrentlyDisplaying() == true
if (isShowing) InlineEditAwaiter.Result.SuggestionProvided else InlineEditAwaiter.Result.NothingProvided
}
}
}
@@ -11,8 +11,9 @@ import com.intellij.util.concurrency.annotations.RequiresEdt
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.TestOnly
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.AtomicReference
@@ -34,8 +35,6 @@ sealed interface InlineEditRequestExecutor : Disposable {
@RequiresEdt
fun cancelActiveRequest()
@TestOnly
@RequiresEdt
suspend fun awaitActiveRequest()
companion object {
@@ -51,7 +50,7 @@ private class InlineEditRequestExecutorImpl(parentScope: CoroutineScope) : Inlin
// Timestamps of jobs are required to understand whether we waited for all requests by some moment
private val lastRequestedTimestamp = AtomicLong(0)
private val lastExecutedTimestamp = AtomicLong(0)
private val lastExecutedTimestamp = MutableStateFlow(0L)
private val nextTask = Channel<Request>(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
@@ -115,13 +114,9 @@ private class InlineEditRequestExecutorImpl(parentScope: CoroutineScope) : Inlin
nextTask.trySend(Request.Cancel(lastRequestedTimestamp.incrementAndGet()))
}
@TestOnly
override suspend fun awaitActiveRequest() {
ThreadingAssertions.assertEventDispatchThread()
val currentTimestamp = lastRequestedTimestamp.get()
while (lastExecutedTimestamp.get() < currentTimestamp) {
yield()
}
lastExecutedTimestamp.first { it >= currentTimestamp }
}
override fun dispose() {
@@ -134,7 +129,7 @@ private class InlineEditRequestExecutorImpl(parentScope: CoroutineScope) : Inlin
private fun setLastExecutedToAtLeast(atLeastTimestamp: Long) {
while (true) {
val currentTimestamp = lastExecutedTimestamp.get()
val currentTimestamp = lastExecutedTimestamp.value
if (currentTimestamp >= atLeastTimestamp) {
return
}
@@ -0,0 +1,65 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.inline.edit
import com.intellij.codeInsight.hint.HintManager
import com.intellij.lang.LangBundle
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.progress.coroutineToIndicator
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.util.concurrency.annotations.RequiresEdt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* Awaits all possible inline edit providers (completion and Next Edit). If nothing is shown after they finish,
* the 'No suggestions' hint is shown.
*
* This service is used only after a user requests inline completion explicitly by shortcut.
*/
@Service(Service.Level.PROJECT)
internal class NoInlineEditShownNotifier(private val project: Project, scope: CoroutineScope) : Disposable {
private val hintRequestExecutor = InlineEditRequestExecutor.create(scope)
@RequiresEdt
fun notifyNoSuggestionIfNothingIsShown(editor: Editor) {
val initialEditorState = runReadAction { editor.getState() }
hintRequestExecutor.switchRequest(onJobCreated = {}) {
val result = InlineEditAwaiter.awaitAllInlineEdits(project, editor)
when (result) {
InlineEditAwaiter.Result.SuggestionProvided -> Unit
InlineEditAwaiter.Result.NothingProvided -> {
withContext(Dispatchers.EDT) {
val finalEditorState = runReadAction { editor.getState() }
if (initialEditorState == finalEditorState) {
coroutineToIndicator {
HintManager.getInstance().showInformationHint(editor, LangBundle.message("completion.no.suggestions"), HintManager.ABOVE)
}
}
}
}
}
}
}
override fun dispose() {
Disposer.dispose(hintRequestExecutor)
}
private fun Editor.getState(): EditorState {
return EditorState(document.modificationStamp, caretModel.offset)
}
private data class EditorState(val modificationStamp: Long, val caretOffset: Int)
companion object {
fun getInstance(project: Project): NoInlineEditShownNotifier = project.service()
}
}
@@ -112,5 +112,9 @@
name="inline.completion.editorTypeResolver"
interface="com.intellij.codeInsight.inline.completion.editor.InlineCompletionEditorTypeResolver"
dynamic="true"/>
<extensionPoint
name="inline.edit.awaiter"
interface="com.intellij.codeInsight.inline.edit.InlineEditAwaiter"
dynamic="true"/>
</extensionPoints>
</idea-plugin>
@@ -911,6 +911,12 @@
/>
<typedHandler implementation="com.intellij.codeInsight.inline.completion.listeners.typing.InlineCompletionTypedHandlerDelegate"/>
<inline.edit.awaiter implementation="com.intellij.codeInsight.inline.edit.InlineCompletionAwaiter"/>
<editorActionHandler
action="CallInlineCompletionAction"
id="inline.edit.no.suggestions.hint"
order="last"
implementationClass="com.intellij.codeInsight.inline.completion.action.CallInlineCompletionAction$ShowNoSuggestionsHintIfNeededHandler"/>
<registryKey key="editor.action.caretMovement.UpDownIgnoreSelectionBoundaries" defaultValue="false"
description="Allows to restore the legacy behavior of Up/Down w.r.t. selection boundaries.\n\
@@ -302,7 +302,7 @@
<action id="Floating.CodeToolbar.Disable" class="com.intellij.ui.codeFloatingToolbar.DisableCodeFloatingToolbarAction" />
<group id="InlineCompletion" popup="true">
<action id="CallInlineCompletionAction" class="com.intellij.codeInsight.inline.completion.CallInlineCompletionAction">
<action id="CallInlineCompletionAction" class="com.intellij.codeInsight.inline.completion.action.CallInlineCompletionAction">
<keyboard-shortcut first-keystroke="shift alt BACK_SLASH" keymap="$default"/>
</action>
<action id="InsertInlineCompletionAction" class="com.intellij.codeInsight.inline.completion.InsertInlineCompletionAction">