IJPL-184924 Refactor NavigationService to Support Extension Points

GitOrigin-RevId: e5395b9482f2e694b5889cc57671453d1f17d25b
This commit is contained in:
Dmitrii Morskii
2025-04-23 19:08:53 +00:00
committed by intellij-monorepo-bot
parent 8ec55d96fa
commit 746dbffb69
18 changed files with 324 additions and 143 deletions
@@ -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;
}
@@ -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() {
@@ -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)) {
@@ -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)) {
@@ -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)
}
@@ -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) {
@@ -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<Collection<Usage>> 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<AbstractPopup> 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<Usage, String> 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);
}
}
}
@@ -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<JTable> speedSearch = MySpeedSearch.installOn(this);
speedSearch.setComparator(new SpeedSearchComparator(false));
@@ -175,6 +175,8 @@ public final class ShowUsagesTable extends JBTable implements UiDataProvider {
List<Object> 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);
}
}
}
@@ -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
}
@@ -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)
}
}
}
@@ -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<Navigatable>, options: NavigationOptions): Boolean {
override suspend fun navigate(navigatables: List<Navigatable>, 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<NavigationRequest>
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<NavigationRequest>
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<NavigationHandler>("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<FileNavigator>()
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
}
}
@@ -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);
}
}
@@ -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
@@ -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
}
@@ -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,
@@ -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<Navigatable>, options: NavigationOptions): Boolean
suspend fun navigate(
navigatables: List<Navigatable>,
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<NavigationService>().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<NavigationService>().navigate(request, options, dataContext)
}
}
@RequiresEdt
private fun fetchDataContext(project: Project): DataContext? {
val component = IdeFocusManager.getInstance(project).getFocusOwner()
return component?.let { DataManager.getInstance().getDataContext(it) }
}
@@ -490,6 +490,8 @@
<extensionPoint name="codeInsight.surroundWithRangeAdjuster" interface="com.intellij.codeInsight.generation.surroundWith.SurroundWithRangeAdjuster" dynamic="true"/>
<extensionPoint name="navigation.navigationHandler" interface="com.intellij.platform.ide.navigation.NavigationHandler" dynamic="true"/>
<extensionPoint name="createFromTemplateActionReplacer" interface="com.intellij.ide.fileTemplates.CreateFromTemplateActionReplacer" dynamic="true"/>
<extensionPoint name="filetype.stubBuilder" beanClass="com.intellij.openapi.fileTypes.FileTypeExtensionPoint" dynamic="true">
@@ -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();