From 746dbffb69f56d990c4ba96d3cf982532c76a54d Mon Sep 17 00:00:00 2001 From: Dmitrii Morskii Date: Fri, 17 Jan 2025 11:53:00 +0100 Subject: [PATCH] IJPL-184924 Refactor NavigationService to Support Extension Points GitOrigin-RevId: e5395b9482f2e694b5889cc57671453d1f17d25b --- .../com/intellij/ide/util/EditSourceUtil.java | 6 +- .../navigation/GotoTargetHandler.java | 20 ++++- .../actions/GotoDeclarationOnlyHandler2.kt | 6 +- .../actions/GotoDeclarationOrUsageHandler2.kt | 2 +- .../actions/GotoTypeDeclarationHandler2.kt | 4 +- .../codeInsight/navigation/actions/ide.kt | 53 +++-------- .../find/actions/ShowUsagesAction.java | 39 ++++++-- .../find/actions/ShowUsagesTable.java | 20 +++-- .../intellij/find/actions/ShowUsagesUtils.kt | 43 --------- .../intellij/find/actions/UsageNavigation.kt | 73 +++++++++++++++ .../navigation/impl/IdeNavigationService.kt | 88 ++++++++++++------- .../GotoTestOrCodeHandler.java | 10 ++- .../platform-api/api-dump-experimental.txt | 4 + .../ide/navigation/NavigationHandler.kt | 31 +++++++ .../ide/navigation/NavigationOptions.kt | 5 ++ .../ide/navigation/NavigationService.kt | 59 +++++++++++-- .../src/META-INF/LangExtensionPoints.xml | 2 + .../com/intellij/usageView/UsageViewUtil.java | 2 + 18 files changed, 324 insertions(+), 143 deletions(-) delete mode 100644 platform/lang-impl/src/com/intellij/find/actions/ShowUsagesUtils.kt create mode 100644 platform/lang-impl/src/com/intellij/find/actions/UsageNavigation.kt create mode 100644 platform/platform-api/src/com/intellij/platform/ide/navigation/NavigationHandler.kt diff --git a/platform/lang-api/src/com/intellij/ide/util/EditSourceUtil.java b/platform/lang-api/src/com/intellij/ide/util/EditSourceUtil.java index cc50db4a71e2..b693ab2e77eb 100644 --- a/platform/lang-api/src/com/intellij/ide/util/EditSourceUtil.java +++ b/platform/lang-api/src/com/intellij/ide/util/EditSourceUtil.java @@ -5,12 +5,15 @@ import com.intellij.ide.ui.UISettings; import com.intellij.navigation.NavigationItem; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.fileEditor.OpenFileDescriptor; +import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.GeneratedSourcesFilter; import com.intellij.openapi.util.UserDataHolder; import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.vfs.VFileProperty; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.platform.ide.navigation.NavigationOptions; +import com.intellij.platform.ide.navigation.NavigationServiceKt; import com.intellij.pom.Navigatable; import com.intellij.pom.PomTargetPsiElement; import com.intellij.psi.PsiElement; @@ -93,7 +96,8 @@ public final class EditSourceUtil { public static boolean navigateToPsiElement(@NotNull PsiElement element) { Navigatable descriptor = getDescriptor(element); if (descriptor != null && descriptor.canNavigate()) { - descriptor.navigate(true); + Project project = element.getProject(); + NavigationServiceKt.navigateBlocking(project, descriptor, NavigationOptions.requestFocus(), null); } return true; } diff --git a/platform/lang-impl/src/com/intellij/codeInsight/navigation/GotoTargetHandler.java b/platform/lang-impl/src/com/intellij/codeInsight/navigation/GotoTargetHandler.java index 1f73714b993f..926e373006c1 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/navigation/GotoTargetHandler.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/navigation/GotoTargetHandler.java @@ -31,7 +31,10 @@ import com.intellij.openapi.ui.popup.PopupChooserBuilder; import com.intellij.openapi.util.NlsActions; import com.intellij.openapi.util.NlsContexts; import com.intellij.openapi.util.Ref; +import com.intellij.openapi.wm.IdeFocusManager; +import com.intellij.openapi.wm.IdeFrame; import com.intellij.platform.backend.presentation.TargetPresentation; +import com.intellij.platform.ide.navigation.NavigationOptions; import com.intellij.pom.Navigatable; import com.intellij.psi.*; import com.intellij.psi.util.PsiUtilCore; @@ -49,6 +52,8 @@ import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; +import static com.intellij.platform.ide.navigation.NavigationServiceKt.navigateBlocking; + public abstract class GotoTargetHandler implements CodeInsightActionHandler { private static final Logger LOG = Logger.getInstance(GotoTargetHandler.class); private static final PsiElementListCellRenderer ourDefaultTargetElementRenderer = new DefaultPsiElementListCellRenderer(); @@ -156,7 +161,7 @@ public abstract class GotoTargetHandler implements CodeInsightActionHandler { ((AdditionalAction)element.getItem()).execute(); } else { - navigate(project, element, navigatable -> navigateToElement(navigatable)); + navigate(project, element, navigatable -> navigateToElement(project, navigatable)); } } }). @@ -275,12 +280,21 @@ public abstract class GotoTargetHandler implements CodeInsightActionHandler { try (AccessToken ignore = SlowOperations.knownIssue("IDEA-339117, EA-842843")) { if (!descriptor.canNavigate()) return false; } - navigateToElement(descriptor); + navigateToElement(target.getProject(), descriptor); return true; } + @ApiStatus.Obsolete protected void navigateToElement(@NotNull Navigatable descriptor) { - descriptor.navigate(true); + IdeFrame frame = IdeFocusManager.getGlobalInstance().getLastFocusedFrame(); + Project project = frame != null ? frame.getProject() : null; + navigateToElement(project, descriptor); + } + + @ApiStatus.Internal + protected void navigateToElement(@Nullable Project project, @NotNull Navigatable descriptor) { + if (project == null) return; + navigateBlocking(project, descriptor, NavigationOptions.requestFocus(), null); } protected boolean shouldSortTargets() { diff --git a/platform/lang-impl/src/com/intellij/codeInsight/navigation/actions/GotoDeclarationOnlyHandler2.kt b/platform/lang-impl/src/com/intellij/codeInsight/navigation/actions/GotoDeclarationOnlyHandler2.kt index ea21f9f55195..974a1af72c92 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/navigation/actions/GotoDeclarationOnlyHandler2.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/navigation/actions/GotoDeclarationOnlyHandler2.kt @@ -46,7 +46,7 @@ internal class GotoDeclarationOnlyHandler2(private val reporter: GotoDeclaration actionResult.navigationProvider?.let { GTDUCollector.recordNavigated(eventData, it.javaClass) } - navigateRequestLazy(project, actionResult.requestor) + navigateRequestLazy(project, actionResult.requestor, editor) reporter?.reportNavigatedToDeclaration(GotoDeclarationReporter.NavigationType.AUTO, actionResult.navigationProvider) } is MultipleTargets -> { @@ -58,7 +58,7 @@ internal class GotoDeclarationOnlyHandler2(private val reporter: GotoDeclaration navigationProvider?.let { GTDUCollector.recordNavigated(eventData, navigationProvider.javaClass) } - navigateRequestLazy(project, requestor) + navigateRequestLazy(project, requestor, editor) reporter?.reportNavigatedToDeclaration(GotoDeclarationReporter.NavigationType.FROM_POPUP, navigationProvider) } popup.showInBestPositionFor(editor) @@ -71,7 +71,7 @@ internal class GotoDeclarationOnlyHandler2(private val reporter: GotoDeclaration override fun startInWriteAction(): Boolean = false override fun invoke(project: Project, editor: Editor, file: PsiFile) { - if (navigateToLookupItem(project)) { + if (navigateToLookupItem(project, editor)) { return } if (EditorUtil.isCaretInVirtualSpace(editor)) { diff --git a/platform/lang-impl/src/com/intellij/codeInsight/navigation/actions/GotoDeclarationOrUsageHandler2.kt b/platform/lang-impl/src/com/intellij/codeInsight/navigation/actions/GotoDeclarationOrUsageHandler2.kt index c3f5aa23c539..4816db434085 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/navigation/actions/GotoDeclarationOrUsageHandler2.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/navigation/actions/GotoDeclarationOrUsageHandler2.kt @@ -64,7 +64,7 @@ class GotoDeclarationOrUsageHandler2 internal constructor(private val reporter: override fun startInWriteAction(): Boolean = false override fun invoke(project: Project, editor: Editor, file: PsiFile) { - if (navigateToLookupItem(project)) { + if (navigateToLookupItem(project, editor)) { return } if (EditorUtil.isCaretInVirtualSpace(editor)) { diff --git a/platform/lang-impl/src/com/intellij/codeInsight/navigation/actions/GotoTypeDeclarationHandler2.kt b/platform/lang-impl/src/com/intellij/codeInsight/navigation/actions/GotoTypeDeclarationHandler2.kt index 3526200cff39..fae895fa108f 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/navigation/actions/GotoTypeDeclarationHandler2.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/navigation/actions/GotoTypeDeclarationHandler2.kt @@ -46,14 +46,14 @@ internal object GotoTypeDeclarationHandler2 : CodeInsightActionHandler { private fun gotoTypeDeclaration(project: Project, editor: Editor, actionResult: NavigationActionResult) { when (actionResult) { is SingleTarget -> { - navigateRequestLazy(project, actionResult.requestor) + navigateRequestLazy(project, actionResult.requestor, editor) } is MultipleTargets -> { val popup = createTargetPopup( CodeInsightBundle.message("choose.type.popup.title"), actionResult.targets, LazyTargetWithPresentation::presentation ) { (requestor, _) -> - navigateRequestLazy(project, requestor) + navigateRequestLazy(project, requestor, editor) } popup.showInBestPositionFor(editor) } diff --git a/platform/lang-impl/src/com/intellij/codeInsight/navigation/actions/ide.kt b/platform/lang-impl/src/com/intellij/codeInsight/navigation/actions/ide.kt index 2ea85912a783..09479f8ddb2c 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/navigation/actions/ide.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/navigation/actions/ide.kt @@ -6,90 +6,63 @@ import com.intellij.codeInsight.TargetElementUtil import com.intellij.codeInsight.hint.HintManager import com.intellij.codeInsight.lookup.Lookup import com.intellij.codeInsight.lookup.LookupManager -import com.intellij.codeInsight.multiverse.isSharedSourceSupportEnabled import com.intellij.codeInsight.navigation.impl.NavigationRequestor import com.intellij.codeInsight.navigation.impl.gtdTargetNavigatable +import com.intellij.ide.DataManager import com.intellij.ide.IdeEventQueue -import com.intellij.ide.ui.UISettings -import com.intellij.ide.util.PsiNavigationSupport import com.intellij.idea.ActionsBundle import com.intellij.lang.LanguageNamesValidation +import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.actionSystem.ex.ActionUtil.underModalProgress import com.intellij.openapi.editor.Editor -import com.intellij.openapi.fileEditor.FileNavigator -import com.intellij.openapi.fileEditor.OpenFileDescriptor import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory import com.intellij.openapi.project.Project import com.intellij.openapi.util.registry.Registry import com.intellij.platform.backend.navigation.NavigationRequest -import com.intellij.platform.backend.navigation.impl.DirectoryNavigationRequest -import com.intellij.platform.backend.navigation.impl.RawNavigationRequest -import com.intellij.platform.backend.navigation.impl.SharedSourceNavigationRequest -import com.intellij.platform.backend.navigation.impl.SourceNavigationRequest +import com.intellij.platform.ide.navigation.NavigationOptions +import com.intellij.platform.ide.navigation.navigateBlocking import com.intellij.psi.PsiFile import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.ui.EDT import org.jetbrains.annotations.ApiStatus.Internal import java.awt.event.MouseEvent -internal fun navigateToLookupItem(project: Project): Boolean { +internal fun navigateToLookupItem(project: Project, editor: Editor): Boolean { val activeLookup: Lookup? = LookupManager.getInstance(project).activeLookup if (activeLookup == null) { return false } val currentItem = activeLookup.currentItem - navigateRequestLazy(project) { + navigateRequestLazy(project, { TargetElementUtil.targetElementFromLookupElement(currentItem) ?.gtdTargetNavigatable() ?.navigationRequest() - } + }, editor) return true } /** * Obtains a [NavigationRequest] instance from [requestor] on a background thread, and calls [navigateRequest]. */ -internal fun navigateRequestLazy(project: Project, requestor: NavigationRequestor) { +internal fun navigateRequestLazy(project: Project, requestor: NavigationRequestor, editor: Editor) { EDT.assertIsEdt() @Suppress("DialogTitleCapitalization") val request = underModalProgress(project, ActionsBundle.actionText("GotoDeclarationOnly")) { requestor.navigationRequest() } if (request != null) { - navigateRequest(project, request) + val dataContext = editor.component.let { DataManager.getInstance().getDataContext(it) } + navigateRequest(project, request, dataContext = dataContext) } } @Internal @RequiresEdt -fun navigateRequest(project: Project, request: NavigationRequest) { +@JvmOverloads +fun navigateRequest(project: Project, request: NavigationRequest, dataContext: DataContext? = null) { EDT.assertIsEdt() IdeDocumentHistory.getInstance(project).includeCurrentCommandAsNavigation() - when (request) { - is SourceNavigationRequest -> { - // TODO support pure source request without OpenFileDescriptor - val offset = request.offsetMarker?.takeIf { it.isValid }?.startOffset ?: -1 - val openFileDescriptor = if (request is SharedSourceNavigationRequest && isSharedSourceSupportEnabled(project)) { - OpenFileDescriptor(project, request.file, request.context, offset) - } - else { - OpenFileDescriptor(project, request.file, offset) - } - if (UISettings.getInstance().openInPreviewTabIfPossible && Registry.`is`("editor.preview.tab.navigation")) { - openFileDescriptor.isUsePreviewTab = true - } - FileNavigator.getInstance().navigate(openFileDescriptor, true) - } - is DirectoryNavigationRequest -> { - PsiNavigationSupport.getInstance().navigateToDirectory(request.directory, true) - } - is RawNavigationRequest -> { - request.navigatable.navigate(true) - } - else -> { - error("unsupported request ${request.javaClass.name}") - } - } + navigateBlocking(project, request, NavigationOptions.requestFocus(), dataContext) } internal fun notifyNowhereToGo(project: Project, editor: Editor, file: PsiFile, offset: Int) { diff --git a/platform/lang-impl/src/com/intellij/find/actions/ShowUsagesAction.java b/platform/lang-impl/src/com/intellij/find/actions/ShowUsagesAction.java index e3f92f1b8a6f..1dc78020caad 100644 --- a/platform/lang-impl/src/com/intellij/find/actions/ShowUsagesAction.java +++ b/platform/lang-impl/src/com/intellij/find/actions/ShowUsagesAction.java @@ -35,6 +35,9 @@ import com.intellij.openapi.application.*; import com.intellij.openapi.components.Service; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.fileEditor.FileEditor; +import com.intellij.openapi.fileEditor.FileEditorLocation; +import com.intellij.openapi.fileEditor.TextEditor; import com.intellij.openapi.fileEditor.impl.EditorHistoryManager; import com.intellij.openapi.fileEditor.impl.text.AsyncEditorLoader; import com.intellij.openapi.keymap.KeymapUtil; @@ -103,6 +106,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -110,8 +114,6 @@ import java.util.stream.Collectors; import static com.intellij.find.actions.ResolverKt.findShowUsages; import static com.intellij.find.actions.SearchOptionsService.SearchVariant.SHOW_USAGES; import static com.intellij.find.actions.ShowUsagesActionHandler.getSecondInvocationHint; -import static com.intellij.find.actions.ShowUsagesUtilsKt.getEditorFor; -import static com.intellij.find.actions.ShowUsagesUtilsKt.navigateAndHint; import static com.intellij.find.findUsages.FindUsagesHandlerFactory.OperationMode.USAGES_WITH_DEFAULT_OPTIONS; import static com.intellij.util.FindUsagesScopeKt.FindUsagesScope; import static com.intellij.util.ObjectUtils.doIfNotNull; @@ -529,6 +531,19 @@ public final class ShowUsagesAction extends AnAction implements PopupAction, Hin return ShowUsagesManager.getInstance(project).showElementUsagesWithResult(parameters, actionHandler, usageView); } + @ApiStatus.Internal + private static Editor getEditorFor(Usage usage) { + FileEditorLocation location = usage.getLocation(); + if (location == null) { + return null; + } + FileEditor newFileEditor = location.getEditor(); + if (newFileEditor instanceof TextEditor fileEditor) { + return fileEditor.getEditor(); + } + return null; + } + public static Future> showElementUsagesWithResult(@NotNull ShowUsagesParameters parameters, @NotNull ShowUsagesActionHandler actionHandler, @NotNull UsageViewImpl usageView) { @@ -567,7 +582,7 @@ public final class ShowUsagesAction extends AnAction implements PopupAction, Hin Runnable itemChosenCallback = table.prepareTable( showMoreUsagesRunnable(parameters, actionHandler), showUsagesInMaximalScopeRunnable(parameters, actionHandler, showUsagesPopupData), - actionHandler + actionHandler, parameters ); Consumer tableResizer = popup -> { @@ -740,6 +755,18 @@ public final class ShowUsagesAction extends AnAction implements PopupAction, Hin // else all usages filtered out } else if (visibleUsages.size() == 1 && actionHandler.navigateToSingleUsageImmediately()) { + final BiConsumer onReady = (Usage usage, @Nls String hint) -> { + var newEditor = getEditorFor(usage); + if (newEditor == null) { + cancel(popup); + return; + } + if (parameters.editor != null) { + hint(false, hint, parameters.withEditor(parameters.editor), actionHandler); + } + cancel(popup); + }; + if (usages.size() == 1) { //the only usage Usage usage = visibleUsages.iterator().next(); @@ -750,7 +777,8 @@ public final class ShowUsagesAction extends AnAction implements PopupAction, Hin } else { String hint = UsageViewBundle.message("show.usages.only.usage", searchScope.getDisplayName()); - navigateAndHint(project, usage, hint, parameters, actionHandler, () -> cancel(popup)); + UsageNavigation.getInstance(project).navigateAndHint( + project, usage, () -> onReady.accept(usage, hint), parameters.editor); } } else { @@ -759,7 +787,8 @@ public final class ShowUsagesAction extends AnAction implements PopupAction, Hin Usage visibleUsage = visibleUsages.iterator().next(); if (areAllUsagesInOneLine(visibleUsage, usages)) { String hint = UsageViewBundle.message("all.usages.are.in.this.line", usages.size(), searchScope.getDisplayName()); - navigateAndHint(project, visibleUsage, hint, parameters, actionHandler, () -> cancel(popup)); + UsageNavigation.getInstance(project).navigateAndHint( + project, visibleUsage, () -> onReady.accept(visibleUsage, hint), parameters.editor); } } } diff --git a/platform/lang-impl/src/com/intellij/find/actions/ShowUsagesTable.java b/platform/lang-impl/src/com/intellij/find/actions/ShowUsagesTable.java index bf6ebf84788b..0dd1cb4af076 100644 --- a/platform/lang-impl/src/com/intellij/find/actions/ShowUsagesTable.java +++ b/platform/lang-impl/src/com/intellij/find/actions/ShowUsagesTable.java @@ -1,21 +1,19 @@ // 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.find.actions; +import com.intellij.ide.DataManager; import com.intellij.ide.util.gotoByName.ModelDiff; -import com.intellij.openapi.actionSystem.CommonDataKeys; -import com.intellij.openapi.actionSystem.DataSink; -import com.intellij.openapi.actionSystem.LangDataKeys; -import com.intellij.openapi.actionSystem.UiDataProvider; +import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.popup.util.PopupUtil; +import com.intellij.platform.ide.navigation.NavigationOptions; import com.intellij.pom.Navigatable; import com.intellij.psi.PsiElement; import com.intellij.ui.*; import com.intellij.ui.popup.HintUpdateSupply; import com.intellij.ui.table.JBTable; import com.intellij.usageView.UsageInfo; -import com.intellij.usageView.UsageViewUtil; import com.intellij.usages.Usage; import com.intellij.usages.UsageInfo2UsageAdapter; import com.intellij.usages.UsageToPsiElementProvider; @@ -44,6 +42,8 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; +import static com.intellij.platform.ide.navigation.NavigationServiceKt.navigateBlocking; + @ApiStatus.Internal public final class ShowUsagesTable extends JBTable implements UiDataProvider { final Usage MORE_USAGES_SEPARATOR = new UsageAdapter(); @@ -93,7 +93,7 @@ public final class ShowUsagesTable extends JBTable implements UiDataProvider { @NotNull Runnable prepareTable(@NotNull Runnable appendMoreUsageRunnable, @NotNull Runnable showInMaximalScopeRunnable, - @NotNull ShowUsagesActionHandler actionHandler) { + @NotNull ShowUsagesActionHandler actionHandler, @NotNull ShowUsagesParameters parameters) { SpeedSearchBase speedSearch = MySpeedSearch.installOn(this); speedSearch.setComparator(new SpeedSearchComparator(false)); @@ -175,6 +175,8 @@ public final class ShowUsagesTable extends JBTable implements UiDataProvider { List usages = selectedUsages.get(); if (usages != null) { for (Object usage : usages) { + DataContext dataContext = parameters.editor != null ? + DataManager.getInstance().getDataContext(parameters.editor.getContentComponent()) : null; if (usage instanceof UsageInfo usageInfo) { PsiElement selectedElement = usageInfo.getElement(); if (selectedElement != null) { @@ -190,10 +192,10 @@ public final class ShowUsagesTable extends JBTable implements UiDataProvider { numberOfLettersTyped, selectedElement.getLanguage(), false); } - UsageViewUtil.navigateTo(usageInfo, true); + UsageNavigation.getInstance(parameters.project).navigate(usageInfo, true, dataContext); } - else if (usage instanceof Navigatable) { - ((Navigatable)usage).navigate(true); + else if (usage instanceof Navigatable navigatable) { + navigateBlocking(parameters.project, navigatable, NavigationOptions.requestFocus(), dataContext); } } } diff --git a/platform/lang-impl/src/com/intellij/find/actions/ShowUsagesUtils.kt b/platform/lang-impl/src/com/intellij/find/actions/ShowUsagesUtils.kt deleted file mode 100644 index d0749c3d85b0..000000000000 --- a/platform/lang-impl/src/com/intellij/find/actions/ShowUsagesUtils.kt +++ /dev/null @@ -1,43 +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.find.actions - -import com.intellij.openapi.application.EDT -import com.intellij.openapi.application.writeIntentReadAction -import com.intellij.openapi.components.ComponentManagerEx -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.fileEditor.TextEditor -import com.intellij.openapi.project.Project -import com.intellij.platform.ide.navigation.NavigationOptions -import com.intellij.platform.ide.navigation.NavigationService -import com.intellij.usages.Usage -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import org.jetbrains.annotations.Nls - -internal fun navigateAndHint(project: Project, - usage: Usage, - hint: @Nls(capitalization = Nls.Capitalization.Sentence) String, - parameters: ShowUsagesParameters, - actionHandler: ShowUsagesActionHandler, - onReady: Runnable) { - // Code below need EDT - (project as ComponentManagerEx).getCoroutineScope().launch(Dispatchers.EDT) { - NavigationService.getInstance(project).navigate(usage, NavigationOptions.defaultOptions().requestFocus(true)) - writeIntentReadAction { - val newEditor = getEditorFor(usage) - if (newEditor == null) { - onReady.run() - return@writeIntentReadAction - } - - ShowUsagesAction.hint(false, hint, parameters.withEditor(newEditor), actionHandler) - onReady.run() - } - } -} - -internal fun getEditorFor(usage: Usage): Editor? { - val location = usage.location - val newFileEditor = location?.editor - return if (newFileEditor is TextEditor) newFileEditor.editor else null -} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/find/actions/UsageNavigation.kt b/platform/lang-impl/src/com/intellij/find/actions/UsageNavigation.kt new file mode 100644 index 000000000000..943844ad319a --- /dev/null +++ b/platform/lang-impl/src/com/intellij/find/actions/UsageNavigation.kt @@ -0,0 +1,73 @@ +// 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.find.actions + +import com.intellij.ide.DataManager +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.readAction +import com.intellij.openapi.application.readActionBlocking +import com.intellij.openapi.application.writeIntentReadAction +import com.intellij.openapi.components.Service +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.platform.backend.navigation.NavigationRequest +import com.intellij.platform.ide.navigation.NavigationOptions +import com.intellij.platform.ide.navigation.NavigationService +import com.intellij.usageView.UsageInfo +import com.intellij.usages.Usage +import com.intellij.usages.impl.UsageViewStatisticsCollector +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.jetbrains.annotations.ApiStatus +import org.jetbrains.annotations.NotNull + +@Service(Service.Level.PROJECT) +@ApiStatus.Internal +class UsageNavigation(private val project: Project, private val cs: CoroutineScope) { + companion object { + @JvmStatic + fun getInstance(project: Project): UsageNavigation = project.getService(UsageNavigation::class.java) + } + + fun navigateAndHint( + project: Project, + usage: Usage, + onReady: Runnable, + editor: Editor?, + ) { + cs.launch(Dispatchers.EDT) { + val dataContext = editor?.let { + DataManager.getInstance().getDataContext(it.component) + } + NavigationService.getInstance(project).navigate(usage, NavigationOptions.requestFocus(), dataContext) + writeIntentReadAction { + onReady.run() + } + } + } + + fun navigate(@NotNull info: UsageInfo, requestFocus: Boolean, dataContext: DataContext?) { + cs.launch { + navigateUsageInfo(info, requestFocus, dataContext) + } + } + + private suspend fun navigateUsageInfo( + info: UsageInfo, + requestFocus: Boolean, + dataContext: DataContext?, + ) { + val request = readAction { + val offset = info.navigationOffset + val project = info.project + val file = info.virtualFile + ?: return@readAction null + NavigationRequest.sourceNavigationRequest(project, file, offset) + } + request?.let { + readActionBlocking { UsageViewStatisticsCollector.logUsageNavigate(project, info) } + NavigationService.getInstance(project).navigate(it, NavigationOptions.defaultOptions().requestFocus(requestFocus), dataContext) + } + } +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/platform/ide/navigation/impl/IdeNavigationService.kt b/platform/lang-impl/src/com/intellij/platform/ide/navigation/impl/IdeNavigationService.kt index f6251515da29..768e2c54555a 100644 --- a/platform/lang-impl/src/com/intellij/platform/ide/navigation/impl/IdeNavigationService.kt +++ b/platform/lang-impl/src/com/intellij/platform/ide/navigation/impl/IdeNavigationService.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.platform.ide.navigation.impl +import com.intellij.codeInsight.multiverse.isSharedSourceSupportEnabled import com.intellij.ide.ui.UISettings import com.intellij.ide.util.PsiNavigationSupport import com.intellij.injected.editor.VirtualFileWindow @@ -11,8 +12,10 @@ import com.intellij.openapi.actionSystem.impl.Utils.isAsyncDataContext import com.intellij.openapi.application.EDT import com.intellij.openapi.application.readAction import com.intellij.openapi.application.writeIntentReadAction +import com.intellij.openapi.components.Service import com.intellij.openapi.components.serviceAsync import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.fileEditor.* import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx import com.intellij.openapi.fileEditor.impl.EditorComposite @@ -27,17 +30,20 @@ import com.intellij.openapi.util.registry.Registry import com.intellij.platform.backend.navigation.NavigationRequest import com.intellij.platform.backend.navigation.impl.DirectoryNavigationRequest import com.intellij.platform.backend.navigation.impl.RawNavigationRequest +import com.intellij.platform.backend.navigation.impl.SharedSourceNavigationRequest import com.intellij.platform.backend.navigation.impl.SourceNavigationRequest +import com.intellij.platform.ide.navigation.NavigationHandler import com.intellij.platform.ide.navigation.NavigationOptions import com.intellij.platform.ide.navigation.NavigationService import com.intellij.platform.util.coroutines.sync.OverflowSemaphore import com.intellij.platform.util.progress.mapWithProgress import com.intellij.pom.Navigatable import com.intellij.util.containers.sequenceOfNotNull -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.* import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.withContext +import kotlin.coroutines.cancellation.CancellationException +@Service(Service.Level.PROJECT) private class IdeNavigationService(private val project: Project) : NavigationService { /** * - `permits = 1` means at any given time only one request is being handled. @@ -64,9 +70,9 @@ private class IdeNavigationService(private val project: Project) : NavigationSer } } - override suspend fun navigate(navigatables: List, options: NavigationOptions): Boolean { + override suspend fun navigate(navigatables: List, options: NavigationOptions, dataContext: DataContext?): Boolean { return semaphore.withPermit { - doNavigate(navigatables, options, dataContext = null) + doNavigate(navigatables, options, dataContext) } } @@ -79,21 +85,9 @@ private class IdeNavigationService(private val project: Project) : NavigationSer return navigate(project = project, requests = requests, options = options, dataContext = dataContext) } - override suspend fun navigate(navigatable: Navigatable, options: NavigationOptions): Boolean { - return semaphore.withPermit { - val request = readAction { - navigatable.navigationRequest() - } ?: return@withPermit false - navigate(project = project, requests = listOf(request), options = options, dataContext = null) - } - } - - override suspend fun navigate(request: NavigationRequest, options: NavigationOptions) { - if (request is SourceNavigationRequest) { - navigateToSource(project = project, request = request, options = options as NavigationOptions.Impl, dataContext = null) - } - else { - navigate(project = project, requests = listOf(request), options = options, dataContext = null) + override suspend fun navigate(request: NavigationRequest, options: NavigationOptions, dataContext: DataContext?) { + semaphore.withPermit { + navigate(project = project, requests = listOf(request), options = options, dataContext = dataContext) } } } @@ -113,7 +107,7 @@ private suspend fun navigate(project: Project, requests: List if (maxSourceRequests in 1..navigatedSourcesCounter) { break } - if (navigateToSource(project = project, request = requestFromNavigatable, options = options, dataContext = dataContext)) { + if (tryNavigateToSource(project = project, request = requestFromNavigatable, options = options, dataContext = dataContext)) { navigatedSourcesCounter++ } else if (nonSourceRequest == null) { @@ -132,15 +126,19 @@ private suspend fun navigate(project: Project, requests: List return true } -private suspend fun navigateToSource( +private suspend fun tryNavigateToSource( project: Project, request: NavigationRequest, options: NavigationOptions.Impl, dataContext: DataContext?, ): Boolean { + if (dataContext != null && executeRequestHandler(request, options, dataContext)) { + return true + } + when (request) { is SourceNavigationRequest -> { - navigateToSource( + navigateToSourceImpl( request = request, options = options, project = project, @@ -185,7 +183,26 @@ private suspend fun navigateNonSource(project: Project, request: NavigationReque } } -private suspend fun navigateToSource( +private val EP_NAME = ExtensionPointName.create("com.intellij.navigation.navigationHandler") + +private fun executeRequestHandler(request: NavigationRequest, options: NavigationOptions, dataContext: DataContext): Boolean { + for (handler in EP_NAME.extensionList) { + try { + if (handler.navigate(request, options, dataContext)) { + return true + } + } + catch (ce: CancellationException) { + throw ce + } + catch (e: Throwable) { + LOG.error("Failed to navigate with $handler", e) + } + } + return false +} + +private suspend fun navigateToSourceImpl( options: NavigationOptions.Impl, request: SourceNavigationRequest, project: Project, @@ -200,20 +217,31 @@ private suspend fun navigateToSource( } } else { - if (dataContext != null) { - val descriptor = OpenFileDescriptor(project, request.file, request.offsetMarker?.takeIf { it.isValid }?.startOffset ?: -1) - descriptor.isUseCurrentWindow = true - if (UISettings.getInstance().openInPreviewTabIfPossible && Registry.`is`("editor.preview.tab.navigation")) { - descriptor.isUsePreviewTab = true - } + val offset = request.offsetMarker?.takeIf { it.isValid }?.startOffset ?: -1 + val inEditorDataContext = dataContext?.let(OpenFileDescriptor.NAVIGATE_IN_EDITOR::getData) != null + val descriptor = if (!inEditorDataContext && request is SharedSourceNavigationRequest && isSharedSourceSupportEnabled(project)) { + OpenFileDescriptor(project, request.file, request.context, offset) + } + else { + OpenFileDescriptor(project, request.file, offset) + } + if (UISettings.getInstance().openInPreviewTabIfPossible && Registry.`is`("editor.preview.tab.navigation")) { + descriptor.isUsePreviewTab = true + } + if (inEditorDataContext) { + descriptor.isUseCurrentWindow = true val fileNavigator = serviceAsync() if (fileNavigator is FileNavigatorImpl && fileNavigator.navigateInRequestedEditorAsync(descriptor, dataContext)) { return } } - if (openFile(request = request, project = project, options = options)) { + // TODO: replace with openFile once IJPL-184882 is fixed + if (FileNavigator.getInstance().canNavigate(descriptor)) { + withContext(Dispatchers.EDT) { + FileNavigator.getInstance().navigate(descriptor, true) + } return } } diff --git a/platform/lang-impl/src/com/intellij/testIntegration/GotoTestOrCodeHandler.java b/platform/lang-impl/src/com/intellij/testIntegration/GotoTestOrCodeHandler.java index 5f1f5a112aa4..c6c73233bbab 100644 --- a/platform/lang-impl/src/com/intellij/testIntegration/GotoTestOrCodeHandler.java +++ b/platform/lang-impl/src/com/intellij/testIntegration/GotoTestOrCodeHandler.java @@ -22,6 +22,7 @@ import com.intellij.psi.PsiFile; import com.intellij.psi.util.PsiUtilCore; import com.intellij.util.ObjectUtils; import com.intellij.util.SmartList; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -149,7 +150,8 @@ public class GotoTestOrCodeHandler extends GotoTargetHandler { } @Override - protected void navigateToElement(@NotNull Navigatable element) { + @ApiStatus.Internal + protected void navigateToElement(@Nullable Project project, @NotNull Navigatable element) { if (element instanceof PsiElement) { NavigationUtil.activateFileWithPsiElement((PsiElement)element, true); } @@ -157,4 +159,10 @@ public class GotoTestOrCodeHandler extends GotoTargetHandler { element.navigate(true); } } + + @Override + @ApiStatus.Obsolete + protected void navigateToElement(@NotNull Navigatable element) { + navigateToElement(null, element); + } } diff --git a/platform/platform-api/api-dump-experimental.txt b/platform/platform-api/api-dump-experimental.txt index c62ef3b87cd1..d5d803e2e133 100644 --- a/platform/platform-api/api-dump-experimental.txt +++ b/platform/platform-api/api-dump-experimental.txt @@ -304,13 +304,17 @@ f:com.intellij.openapi.wm.PermissionsKt - createMainPanelToolbar(com.intellij.openapi.Disposable):javax.swing.JComponent - createQuickAccessActions(com.intellij.openapi.Disposable):java.util.List - createQuickAccessComponent(com.intellij.openapi.Disposable):java.awt.Component +*:com.intellij.platform.ide.navigation.NavigationHandler +- a:navigate(com.intellij.platform.backend.navigation.NavigationRequest,com.intellij.platform.ide.navigation.NavigationOptions,com.intellij.openapi.actionSystem.DataContext):Z *:com.intellij.platform.ide.navigation.NavigationOptions - *sf:Companion:com.intellij.platform.ide.navigation.NavigationOptions$Companion - s:defaultOptions():com.intellij.platform.ide.navigation.NavigationOptions - a:preserveCaret(Z):com.intellij.platform.ide.navigation.NavigationOptions +- s:requestFocus():com.intellij.platform.ide.navigation.NavigationOptions - a:requestFocus(Z):com.intellij.platform.ide.navigation.NavigationOptions *f:com.intellij.platform.ide.navigation.NavigationOptions$Companion - f:defaultOptions():com.intellij.platform.ide.navigation.NavigationOptions +- f:requestFocus():com.intellij.platform.ide.navigation.NavigationOptions *:com.intellij.platform.ide.navigation.NavigationService - *sf:Companion:com.intellij.platform.ide.navigation.NavigationService$Companion - s:getInstance(com.intellij.openapi.project.Project):com.intellij.platform.ide.navigation.NavigationService diff --git a/platform/platform-api/src/com/intellij/platform/ide/navigation/NavigationHandler.kt b/platform/platform-api/src/com/intellij/platform/ide/navigation/NavigationHandler.kt new file mode 100644 index 000000000000..b6b10e354933 --- /dev/null +++ b/platform/platform-api/src/com/intellij/platform/ide/navigation/NavigationHandler.kt @@ -0,0 +1,31 @@ +// 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.platform.ide.navigation + +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.platform.backend.navigation.NavigationRequest +import org.jetbrains.annotations.ApiStatus.Experimental + +/** + * Handles navigation requests in the IDE. + * + * Implementations of this interface can be registered via the "com.intellij.navigation.navigationHandler" extension point + * to provide custom navigation behavior for specific types of [com.intellij.platform.backend.navigation.NavigationRequest]s. + * + * The navigation system iterates through all registered handlers and calls their [navigate] method until one of them + * returns true, indicating that the navigation request has been handled. + */ +@Experimental +interface NavigationHandler { + + /** + * Attempts to handle the given navigation request. + * + * @param request The navigation request to handle. This can be a source navigation request (to navigate to a specific + * location in a file), a directory navigation request, or a custom implementation. + * @param options Options that control the navigation behavior, such as whether to request focus, preserve caret position, etc. + * @param dataContext The data context from which the navigation was initiated, which may contain additional information + * needed for navigation. + * @return true if the navigation request was handled by this handler, false otherwise. + */ + fun navigate(request: NavigationRequest, options: NavigationOptions, dataContext: DataContext): Boolean +} \ No newline at end of file diff --git a/platform/platform-api/src/com/intellij/platform/ide/navigation/NavigationOptions.kt b/platform/platform-api/src/com/intellij/platform/ide/navigation/NavigationOptions.kt index 2806695a1082..0a274b2ec4b7 100644 --- a/platform/platform-api/src/com/intellij/platform/ide/navigation/NavigationOptions.kt +++ b/platform/platform-api/src/com/intellij/platform/ide/navigation/NavigationOptions.kt @@ -51,6 +51,11 @@ interface NavigationOptions { @JvmStatic fun defaultOptions(): NavigationOptions = defaultOptions + @JvmStatic + fun requestFocus(): NavigationOptions { + return defaultOptions().requestFocus(true) + } + private val defaultOptions = Impl( requestFocus = true, preserveCaret = false, diff --git a/platform/platform-api/src/com/intellij/platform/ide/navigation/NavigationService.kt b/platform/platform-api/src/com/intellij/platform/ide/navigation/NavigationService.kt index 030be81e55b0..22e5070775f1 100644 --- a/platform/platform-api/src/com/intellij/platform/ide/navigation/NavigationService.kt +++ b/platform/platform-api/src/com/intellij/platform/ide/navigation/NavigationService.kt @@ -1,13 +1,18 @@ // 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.platform.ide.navigation +import com.intellij.ide.DataManager +import com.intellij.ide.IdeBundle import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.components.service +import com.intellij.openapi.components.serviceAsync import com.intellij.openapi.project.Project +import com.intellij.openapi.wm.IdeFocusManager import com.intellij.platform.backend.navigation.NavigationRequest +import com.intellij.platform.ide.progress.runWithModalProgressBlocking import com.intellij.pom.Navigatable -import org.jetbrains.annotations.ApiStatus.Experimental -import org.jetbrains.annotations.ApiStatus.Internal +import com.intellij.util.concurrency.annotations.RequiresEdt +import org.jetbrains.annotations.ApiStatus.* @Experimental interface NavigationService { @@ -21,11 +26,55 @@ interface NavigationService { suspend fun navigate(dataContext: DataContext, options: NavigationOptions) @Internal - suspend fun navigate(request: NavigationRequest, options: NavigationOptions = NavigationOptions.defaultOptions()) + suspend fun navigate( + request: NavigationRequest, + options: NavigationOptions = NavigationOptions.defaultOptions(), + dataContext: DataContext? = null, + ) @Internal // compatibility function - suspend fun navigate(navigatables: List, options: NavigationOptions): Boolean + suspend fun navigate( + navigatables: List, + options: NavigationOptions = NavigationOptions.defaultOptions(), + dataContext: DataContext? = null, + ): Boolean @Internal // compatibility function - suspend fun navigate(navigatable: Navigatable, options: NavigationOptions = NavigationOptions.defaultOptions()): Boolean + suspend fun navigate(navigatable: Navigatable, options: NavigationOptions, dataContext: DataContext? = null): Boolean { + return navigate(listOf(navigatable), options, dataContext) + } +} + +/** + * Navigates to the specified [navigatable] in a blocking manner, showing a modal progress dialog. + * This is a blocking version of [NavigationService.navigate]. + */ +@RequiresEdt +@Obsolete +@Internal +fun navigateBlocking(project: Project, navigatable: Navigatable, options: NavigationOptions, dataContext: DataContext?) { + val dataContext = dataContext ?: fetchDataContext(project) + return runWithModalProgressBlocking(project, IdeBundle.message("progress.title.preparing.navigation")) { + project.serviceAsync().navigate(navigatable, options, dataContext) + } +} + +/** + * Navigates to the specified navigation [request] in a blocking manner, showing a modal progress dialog. + * This is a blocking version of [NavigationService.navigate]. + */ +@RequiresEdt +@Obsolete +@Internal +fun navigateBlocking(project: Project, request: NavigationRequest, options: NavigationOptions, dataContext: DataContext?) { + val dataContext = dataContext ?: fetchDataContext(project) + return runWithModalProgressBlocking(project, IdeBundle.message("progress.title.preparing.navigation")) { + project.serviceAsync().navigate(request, options, dataContext) + } +} + +@RequiresEdt +private fun fetchDataContext(project: Project): DataContext? { + val component = IdeFocusManager.getInstance(project).getFocusOwner() + return component?.let { DataManager.getInstance().getDataContext(it) } } diff --git a/platform/platform-resources/src/META-INF/LangExtensionPoints.xml b/platform/platform-resources/src/META-INF/LangExtensionPoints.xml index 3f02f3a55b96..d70746cce79b 100644 --- a/platform/platform-resources/src/META-INF/LangExtensionPoints.xml +++ b/platform/platform-resources/src/META-INF/LangExtensionPoints.xml @@ -490,6 +490,8 @@ + + diff --git a/platform/usageView/src/com/intellij/usageView/UsageViewUtil.java b/platform/usageView/src/com/intellij/usageView/UsageViewUtil.java index e7587569656c..93318a5ef39d 100644 --- a/platform/usageView/src/com/intellij/usageView/UsageViewUtil.java +++ b/platform/usageView/src/com/intellij/usageView/UsageViewUtil.java @@ -27,6 +27,7 @@ import com.intellij.usages.UsageInfo2UsageAdapter; import com.intellij.usages.UsageView; import com.intellij.usages.impl.UsageViewStatisticsCollector; import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; @@ -139,6 +140,7 @@ public final class UsageViewUtil { return ContainerUtil.map2Array(usageInfos, PsiElement.class, UsageInfo::getElement); } + @ApiStatus.Obsolete public static void navigateTo(@NotNull UsageInfo info, boolean requestFocus) { int offset = info.getNavigationOffset(); VirtualFile file = info.getVirtualFile();