diff --git a/platform/inline-completion/shared/src/InlineCompletionHandlerImpl.kt b/platform/inline-completion/shared/src/InlineCompletionHandlerImpl.kt index 385442f6b168..b37666027c02 100644 --- a/platform/inline-completion/shared/src/InlineCompletionHandlerImpl.kt +++ b/platform/inline-completion/shared/src/InlineCompletionHandlerImpl.kt @@ -20,7 +20,6 @@ internal class InlineCompletionHandlerImpl( ) : InlineCompletionHandler(scope, editor, parentDisposable) { init { - addEventListener(InlineCompletionNoSuggestionsListener(editor)) InlineCompletionOnboardingListener.createIfOnboarding(editor)?.let(::addEventListener) } diff --git a/platform/inline-completion/shared/src/InlineCompletionNoSuggestionsListener.kt b/platform/inline-completion/shared/src/InlineCompletionNoSuggestionsListener.kt deleted file mode 100644 index 50e78f0abd68..000000000000 --- a/platform/inline-completion/shared/src/InlineCompletionNoSuggestionsListener.kt +++ /dev/null @@ -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) - } - } - } -} diff --git a/platform/platform-impl/codeinsight-inline/src/com/intellij/codeInsight/inline/completion/InlineCompletionActions.kt b/platform/platform-impl/codeinsight-inline/src/com/intellij/codeInsight/inline/completion/InlineCompletionActions.kt index 94eaeaf35bb4..0adbc092547d 100644 --- a/platform/platform-impl/codeinsight-inline/src/com/intellij/codeInsight/inline/completion/InlineCompletionActions.kt +++ b/platform/platform-impl/codeinsight-inline/src/com/intellij/codeInsight/inline/completion/InlineCompletionActions.kt @@ -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 { diff --git a/platform/platform-impl/codeinsight-inline/src/com/intellij/codeInsight/inline/completion/InlineCompletionActionsPromoter.kt b/platform/platform-impl/codeinsight-inline/src/com/intellij/codeInsight/inline/completion/InlineCompletionActionsPromoter.kt index c8a7b2886cd6..f0ca62140d4c 100644 --- a/platform/platform-impl/codeinsight-inline/src/com/intellij/codeInsight/inline/completion/InlineCompletionActionsPromoter.kt +++ b/platform/platform-impl/codeinsight-inline/src/com/intellij/codeInsight/inline/completion/InlineCompletionActionsPromoter.kt @@ -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 diff --git a/platform/platform-impl/codeinsight-inline/src/com/intellij/codeInsight/inline/completion/InlineCompletionHandler.kt b/platform/platform-impl/codeinsight-inline/src/com/intellij/codeInsight/inline/completion/InlineCompletionHandler.kt index 5daa4f1e1c79..e44ac04ddbde 100644 --- a/platform/platform-impl/codeinsight-inline/src/com/intellij/codeInsight/inline/completion/InlineCompletionHandler.kt +++ b/platform/platform-impl/codeinsight-inline/src/com/intellij/codeInsight/inline/completion/InlineCompletionHandler.kt @@ -590,9 +590,7 @@ abstract class InlineCompletionHandler @ApiStatus.Internal constructor( // ----------------------------------- - @TestOnly suspend fun awaitExecution() { - ThreadingAssertions.assertEventDispatchThread() executor.awaitActiveRequest() } diff --git a/platform/platform-impl/codeinsight-inline/src/com/intellij/codeInsight/inline/completion/action/CallInlineCompletionAction.kt b/platform/platform-impl/codeinsight-inline/src/com/intellij/codeInsight/inline/completion/action/CallInlineCompletionAction.kt new file mode 100644 index 000000000000..1e3faec90d77 --- /dev/null +++ b/platform/platform-impl/codeinsight-inline/src/com/intellij/codeInsight/inline/completion/action/CallInlineCompletionAction.kt @@ -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) + } + } +} diff --git a/platform/platform-impl/codeinsight-inline/src/com/intellij/codeInsight/inline/edit/InlineEditAwaiter.kt b/platform/platform-impl/codeinsight-inline/src/com/intellij/codeInsight/inline/edit/InlineEditAwaiter.kt new file mode 100644 index 000000000000..69b294583a10 --- /dev/null +++ b/platform/platform-impl/codeinsight-inline/src/com/intellij/codeInsight/inline/edit/InlineEditAwaiter.kt @@ -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("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 + } + } +} diff --git a/platform/platform-impl/codeinsight-inline/src/com/intellij/codeInsight/inline/edit/InlineEditRequestExecutor.kt b/platform/platform-impl/codeinsight-inline/src/com/intellij/codeInsight/inline/edit/InlineEditRequestExecutor.kt index e955224ec8bd..26dfe9b20187 100644 --- a/platform/platform-impl/codeinsight-inline/src/com/intellij/codeInsight/inline/edit/InlineEditRequestExecutor.kt +++ b/platform/platform-impl/codeinsight-inline/src/com/intellij/codeInsight/inline/edit/InlineEditRequestExecutor.kt @@ -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(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 } diff --git a/platform/platform-impl/codeinsight-inline/src/com/intellij/codeInsight/inline/edit/NoInlineEditShownNotifier.kt b/platform/platform-impl/codeinsight-inline/src/com/intellij/codeInsight/inline/edit/NoInlineEditShownNotifier.kt new file mode 100644 index 000000000000..39277429e11a --- /dev/null +++ b/platform/platform-impl/codeinsight-inline/src/com/intellij/codeInsight/inline/edit/NoInlineEditShownNotifier.kt @@ -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() + } +} diff --git a/platform/platform-resources/src/META-INF/EditorExtensionPoints.xml b/platform/platform-resources/src/META-INF/EditorExtensionPoints.xml index 260840f7d986..d7795c26b306 100644 --- a/platform/platform-resources/src/META-INF/EditorExtensionPoints.xml +++ b/platform/platform-resources/src/META-INF/EditorExtensionPoints.xml @@ -112,5 +112,9 @@ name="inline.completion.editorTypeResolver" interface="com.intellij.codeInsight.inline.completion.editor.InlineCompletionEditorTypeResolver" dynamic="true"/> + diff --git a/platform/platform-resources/src/META-INF/PlatformExtensions.xml b/platform/platform-resources/src/META-INF/PlatformExtensions.xml index 30c2447ae7e8..cd95ecc209bc 100644 --- a/platform/platform-resources/src/META-INF/PlatformExtensions.xml +++ b/platform/platform-resources/src/META-INF/PlatformExtensions.xml @@ -911,6 +911,12 @@ /> + + - +