IJPL-7 IDEA-304701 prepare navigation request in BG (parse/decompile), handle it on EDT

GitOrigin-RevId: f75a04052061317c1a0eff0074efca24e5355bef
This commit is contained in:
Daniil Ovchinnikov
2023-05-13 20:42:46 +00:00
committed by intellij-monorepo-bot
parent b9f4b8a803
commit 3d5155fe07
12 changed files with 430 additions and 5 deletions
@@ -34,6 +34,8 @@ interface NavigationRequest {
}
/**
* @param elementRange is used to determine whether
* to preserve the caret if [preserveCaret][com.intellij.platform.ide.navigation.NavigationOptions.preserveCaret] is set
* @return a request for the navigation to the [start offset][TextRange.getStartOffset] of [elementRange],
* or `null` if the navigation is not possible for any reason
*/
@@ -48,6 +48,7 @@ import com.intellij.ui.popup.list.ListPopupImpl;
import com.intellij.ui.popup.list.PopupListElementRenderer;
import com.intellij.util.Processor;
import com.intellij.util.TextWithIcon;
import com.intellij.util.ui.EDT;
import com.intellij.util.ui.JBUI;
import org.jetbrains.annotations.ApiStatus.Internal;
import org.jetbrains.annotations.NotNull;
@@ -222,13 +223,15 @@ public final class NavigationUtil {
return activateFileIfOpen(project, vFile, element.getTextRange(), searchForOpen, requestFocus);
}
private static boolean activateFileIfOpen(
@Internal
public static boolean activateFileIfOpen(
@NotNull Project project,
@NotNull VirtualFile vFile,
@Nullable TextRange range,
boolean searchForOpen,
boolean requestFocus
) {
EDT.assertIsEdt();
if (!EditorHistoryManager.getInstance(project).hasBeenOpen(vFile)) {
return false;
}
@@ -1,4 +1,4 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.projectView.impl.nodes;
@@ -27,6 +27,7 @@ import com.intellij.openapi.vcs.FileStatus;
import com.intellij.openapi.vcs.FileStatusManager;
import com.intellij.openapi.vfs.VFileProperty;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.platform.backend.navigation.NavigationRequest;
import com.intellij.pom.StatePreservingNavigatable;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
@@ -37,6 +38,8 @@ import com.intellij.ui.LayeredIcon;
import com.intellij.util.AstLoadingFilter;
import com.intellij.util.IconUtil;
import com.intellij.util.PlatformIcons;
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread;
import com.intellij.util.concurrency.annotations.RequiresReadLock;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -288,6 +291,17 @@ public abstract class AbstractPsiBasedNode<Value> extends ProjectViewNode<Value>
return psiElement instanceof NavigationItem ? (NavigationItem) psiElement : null;
}
@RequiresReadLock
@RequiresBackgroundThread
@Override
public @Nullable NavigationRequest navigationRequest() {
PsiElement element = extractPsiFromValue();
if (element == null) {
return null;
}
return ((NavigationItem)element).navigationRequest();
}
@Override
public void navigate(boolean requestFocus, boolean preserveState) {
if (canNavigate()) {
@@ -8,6 +8,12 @@ import com.intellij.pom.Navigatable
import com.intellij.psi.PsiDirectory
import org.jetbrains.annotations.ApiStatus.Internal
/**
* @param offsetMarker desired caret position, or `null` to keep the position unchanged
* @param elementRangeMarker marker of a range, where the existing caret should remain unchanged
* if [com.intellij.platform.ide.navigation.NavigationOptions.preserveCaret] is set,
* or `null` to change the caret position according to [offsetMarker]
*/
@Internal
class SourceNavigationRequest internal constructor(
val file: VirtualFile,
@@ -0,0 +1,210 @@
// Copyright 2000-2023 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.navigation.NavigationUtil.activateFileIfOpen
import com.intellij.codeInsight.navigation.NavigationUtil.shouldOpenAsNative
import com.intellij.ide.ui.UISettings
import com.intellij.ide.util.PsiNavigationSupport
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.impl.Utils.isAsyncDataContext
import com.intellij.openapi.actionSystem.impl.Utils.wrapToAsyncDataContext
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.readAction
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.fileEditor.OpenFileDescriptor
import com.intellij.openapi.progress.blockingContext
import com.intellij.openapi.progress.mapWithProgress
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.SourceNavigationRequest
import com.intellij.platform.ide.navigation.NavigationOptions
import com.intellij.platform.ide.navigation.NavigationService
import com.intellij.pom.Navigatable
import com.intellij.util.OverflowSemaphore
import com.intellij.util.ui.EDT
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus.Internal
@Internal
internal class IdeNavigationService(private val project: Project) : NavigationService {
/**
* - `permits = 1` means at any given time only 1 request is being handled.
* - [BufferOverflow.DROP_OLDEST] makes each new navigation request cancel the previous one.
*/
private val semaphore: OverflowSemaphore = OverflowSemaphore(permits = 1, overflow = BufferOverflow.DROP_OLDEST)
override suspend fun navigate(ctx: DataContext, options: NavigationOptions) {
if (!isAsyncDataContext(ctx)) {
LOG.error("Expected async context, got: $ctx")
val asyncContext = withContext(Dispatchers.EDT) {
// hope that context component is still available
wrapToAsyncDataContext(ctx)
}
navigate(asyncContext, options)
}
return semaphore.withPermit {
val navigatables = readAction {
ctx.getData(CommonDataKeys.NAVIGATABLE_ARRAY)
}
if (!navigatables.isNullOrEmpty()) {
doNavigate(navigatables.toList(), options)
}
}
}
override suspend fun navigate(navigatables: List<Navigatable>, options: NavigationOptions): Boolean {
return semaphore.withPermit {
doNavigate(navigatables, options)
}
}
private suspend fun doNavigate(navigatables: List<Navigatable>, options: NavigationOptions): Boolean {
val requests = navigatables.mapWithProgress(concurrent = true) {
readAction {
it.navigationRequest()
}
}.filterNotNull()
return withContext(Dispatchers.EDT) {
blockingContext {
navigate(project, requests, options)
}
}
}
override suspend fun navigate(navigatable: Navigatable, options: NavigationOptions): Boolean {
return semaphore.withPermit {
val request = readAction {
navigatable.navigationRequest()
}
if (request == null) {
false
}
else {
withContext(Dispatchers.EDT) {
blockingContext {
navigateToSource(project, request, options as NavigationOptions.Impl)
}
}
}
}
}
}
internal val LOG = Logger.getInstance("#com.intellij.platform.ide.navigation.impl")
/**
* Navigates to all sources from [requests], or navigates to first non-source request.
*/
private fun navigate(project: Project, requests: List<NavigationRequest>, options: NavigationOptions): Boolean {
EDT.assertIsEdt()
val maxSourceRequests = Registry.intValue("ide.source.file.navigation.limit", 100)
var nonSourceRequest: NavigationRequest? = null
options as NavigationOptions.Impl
var navigatedSourcesCounter = 0
for (requestFromNavigatable in requests) {
if (maxSourceRequests in 1..navigatedSourcesCounter) {
break
}
if (navigateToSource(project, requestFromNavigatable, options)) {
navigatedSourcesCounter++
}
else if (nonSourceRequest == null) {
nonSourceRequest = requestFromNavigatable
}
}
if (navigatedSourcesCounter > 0) {
return true
}
if (nonSourceRequest == null) {
return false
}
navigateNonSource(nonSourceRequest, options)
return true
}
private fun navigateToSource(project: Project, request: NavigationRequest, options: NavigationOptions.Impl): Boolean {
EDT.assertIsEdt()
when (request) {
is SourceNavigationRequest -> {
navigateToSource(project, request, options)
return true
}
is DirectoryNavigationRequest -> {
return false
}
is RawNavigationRequest -> {
if (request.canNavigateToSource) {
request.navigatable.navigate(options.requestFocus)
return true
}
else {
return false
}
}
else -> {
error("Unsupported request: $request")
}
}
}
private fun navigateToSource(project: Project, request: SourceNavigationRequest, options: NavigationOptions.Impl) {
EDT.assertIsEdt()
if (tryActivateOpenFile(project, request, options)) {
return
}
// TODO support pure source request without OpenFileDescriptor
val offset = request.elementRangeMarker?.takeIf { it.isValid }?.startOffset ?: -1
val openFileDescriptor = OpenFileDescriptor(project, request.file, offset)
openFileDescriptor.isUseCurrentWindow = true
if (UISettings.getInstance().openInPreviewTabIfPossible && Registry.`is`("editor.preview.tab.navigation")) {
openFileDescriptor.isUsePreviewTab = true
}
openFileDescriptor.navigate(options.requestFocus)
}
private fun tryActivateOpenFile(
project: Project,
request: SourceNavigationRequest,
options: NavigationOptions.Impl,
): Boolean {
if (!options.preserveCaret && !options.requestFocus) {
return false
}
if (shouldOpenAsNative(request.file)) {
return false
}
val elementRangeMarker = request.elementRangeMarker
if (elementRangeMarker == null || !elementRangeMarker.isValid) {
return false
}
val elementRange = elementRangeMarker.textRange
return activateFileIfOpen(project, request.file, elementRange, options.requestFocus, options.requestFocus)
}
private fun navigateNonSource(request: NavigationRequest, options: NavigationOptions.Impl) {
EDT.assertIsEdt()
return when (request) {
is DirectoryNavigationRequest -> {
PsiNavigationSupport.getInstance().navigateToDirectory(request.directory, options.requestFocus)
}
is RawNavigationRequest -> {
check(!request.canNavigateToSource)
request.navigatable.navigate(options.requestFocus)
}
else -> {
error("Non-source request expected here, got: $request")
}
}
}
@@ -0,0 +1,62 @@
// Copyright 2000-2023 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 org.jetbrains.annotations.ApiStatus.Experimental
import org.jetbrains.annotations.ApiStatus.Internal
@Experimental
interface NavigationOptions {
/**
* Sets whether to request the focus.
*
* Default: `true`.
*/
fun requestFocus(value: Boolean): NavigationOptions
/**
* If the navigation leads to a file, which is already open in some editor,
* the editor will be focused, but the caret position will remain unchanged,
* if the caret position is within text range of requested PsiElement.
*
* For example, when requesting navigation to PsiElement, which corresponds to class `C`:
* ```
* <caret>package com.foo.bar;
* class C { }
* ```
* the caret will be placed here:
* ```
* package com.foo.bar;
* class <caret>C { }
* ```
* But if the caret was already inside the [element range][com.intellij.platform.backend.navigation.impl.SourceNavigationRequest.elementRangeMarker],
* it will remain unchanged:
* ```
* package com.foo.bar;
* class C { <caret> }
* ```
*
* Default: `false`.
*/
fun preserveCaret(value: Boolean): NavigationOptions
companion object {
@JvmStatic
fun defaultOptions(): NavigationOptions = defaultOptions
private val defaultOptions = Impl(
requestFocus = true,
preserveCaret = false,
)
}
@Internal
data class Impl internal constructor(
val requestFocus: Boolean,
val preserveCaret: Boolean,
) : NavigationOptions {
override fun requestFocus(value: Boolean): NavigationOptions = copy(requestFocus = value)
override fun preserveCaret(value: Boolean): NavigationOptions = copy(preserveCaret = value)
}
}
@@ -0,0 +1,29 @@
// Copyright 2000-2023 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.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.pom.Navigatable
import org.jetbrains.annotations.ApiStatus.Experimental
import org.jetbrains.annotations.ApiStatus.Internal
@Experimental
interface NavigationService {
companion object {
@JvmStatic
fun getInstance(project: Project): NavigationService {
return project.service<NavigationService>()
}
}
suspend fun navigate(ctx: DataContext, options: NavigationOptions)
@Internal // compatibility function
suspend fun navigate(navigatables: List<Navigatable>, options: NavigationOptions): Boolean
@Internal // compatibility function
suspend fun navigate(navigatable: Navigatable, options: NavigationOptions): Boolean
}
@@ -1,12 +1,14 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.util
import com.intellij.ide.DataManager
import com.intellij.ide.util.treeView.NodeDescriptor
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.application.AccessToken
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.registry.Registry
import com.intellij.ui.ClientProperty
import com.intellij.ui.DoubleClickListener
import com.intellij.ui.treeStructure.treetable.TreeTable
@@ -176,7 +178,13 @@ object EditSourceOnDoubleClickHandler {
}
protected open fun processDoubleClick(e: MouseEvent, dataContext: DataContext, treePath: TreePath) {
SlowOperations.knownIssue("IDEA-304701, EA-659716").use {
val token = if (Registry.`is`("ide.navigation.requests")) {
AccessToken.EMPTY_ACCESS_TOKEN
}
else {
SlowOperations.knownIssue("IDEA-304701, EA-659716")
}
token.use {
OpenSourceUtil.openSourcesFrom(dataContext, true)
}
whenPerformed?.run()
@@ -4,6 +4,7 @@ package com.intellij.util;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.DataProvider;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.pom.Navigatable;
import com.intellij.pom.StatePreservingNavigatable;
@@ -17,6 +18,10 @@ public final class OpenSourceUtil {
}
public static void openSourcesFrom(@NotNull DataContext context, boolean requestFocus) {
if (Registry.is("ide.navigation.requests")) {
OpenSourceUtilKt.openSourcesFrom(context, requestFocus);
return;
}
navigate(requestFocus, false, CommonDataKeys.NAVIGATABLE_ARRAY.getData(context));
}
@@ -81,6 +86,12 @@ public final class OpenSourceUtil {
if (navigatables == null) {
return false;
}
if (Registry.is("ide.navigation.requests")) {
Project project = OpenSourceUtilKt.findProject(navigatables);
if (project != null) {
return OpenSourceUtilKt.navigate(project, requestFocus, tryNotToScroll, navigatables);
}
}
Navigatable nonSourceNavigatable = null;
@@ -139,7 +150,16 @@ public final class OpenSourceUtil {
* @return {@code true} if navigation is done, {@code false} otherwise
*/
public static boolean navigateToSource(boolean requestFocus, boolean tryNotToScroll, @Nullable Navigatable navigatable) {
if (navigatable == null || !navigatable.canNavigateToSource()) {
if (navigatable == null) {
return false;
}
if (Registry.is("ide.navigation.requests")) {
Project project = OpenSourceUtilKt.findProject(navigatable);
if (project != null) {
return OpenSourceUtilKt.navigateToSource(project, requestFocus, tryNotToScroll, navigatable);
}
}
if (!navigatable.canNavigateToSource()) {
return false;
}
if (tryNotToScroll && navigatable instanceof StatePreservingNavigatable) {
@@ -0,0 +1,67 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:Internal
package com.intellij.util
import com.intellij.ide.IdeBundle
import com.intellij.ide.ui.IdeUiService
import com.intellij.ide.util.treeView.NodeDescriptor
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.fileEditor.OpenFileDescriptor
import com.intellij.openapi.progress.runBlockingModal
import com.intellij.openapi.project.Project
import com.intellij.platform.ide.navigation.NavigationOptions
import com.intellij.platform.ide.navigation.NavigationService
import com.intellij.pom.Navigatable
import com.intellij.psi.PsiElement
import org.jetbrains.annotations.ApiStatus.Internal
internal fun openSourcesFrom(context: DataContext, requestFocus: Boolean) {
val project = context.getData(CommonDataKeys.PROJECT) ?: return
val asyncContext = IdeUiService.getInstance().createAsyncDataContext(context)
val options = NavigationOptions.defaultOptions().requestFocus(requestFocus)
runBlockingModal(project, IdeBundle.message("progress.title.preparing.navigation")) {
NavigationService.getInstance(project).navigate(asyncContext, options)
}
}
internal fun navigate(project: Project, requestFocus: Boolean, tryNotToScroll: Boolean, navigatables: Iterable<Navigatable?>?): Boolean {
if (navigatables == null) {
return false
}
val filteredNavigatables = navigatables.filterNotNull()
val options = NavigationOptions.defaultOptions().requestFocus(requestFocus).preserveCaret(tryNotToScroll)
return runBlockingModal(project, IdeBundle.message("progress.title.preparing.navigation")) {
NavigationService.getInstance(project).navigate(filteredNavigatables, options)
}
}
/**
* Navigates to source of the specified navigatable.
*
* @param requestFocus specifies whether a focus should be requested or not
* @param tryNotToScroll specifies whether a corresponding editor should preserve its state if it is possible
* @return `true` if navigation is done, `false` otherwise
*/
internal fun navigateToSource(project: Project, requestFocus: Boolean, tryNotToScroll: Boolean, navigatable: Navigatable): Boolean {
val options = NavigationOptions.defaultOptions().requestFocus(requestFocus).preserveCaret(tryNotToScroll)
return runBlockingModal(project, IdeBundle.message("progress.title.preparing.navigation")) {
NavigationService.getInstance(project).navigate(navigatable, options)
}
}
internal fun findProject(navigatables: Iterable<Navigatable>): Project? {
return navigatables.firstNotNullOfOrNull {
findProject(it)
}
}
internal fun findProject(navigatable: Navigatable): Project? {
return when (navigatable) {
is PsiElement -> navigatable.project
is OpenFileDescriptor -> navigatable.project
is NodeDescriptor<*> -> navigatable.project
else -> null
}
}
@@ -246,6 +246,8 @@
<applicationService serviceInterface="com.intellij.platform.backend.navigation.NavigationRequests"
serviceImplementation="com.intellij.platform.backend.navigation.impl.NavigationRequestsImpl"/>
<projectService serviceInterface="com.intellij.platform.ide.navigation.NavigationService"
serviceImplementation="com.intellij.platform.ide.navigation.impl.IdeNavigationService"/>
<applicationService serviceInterface="com.intellij.ide.util.PsiNavigationSupport"
serviceImplementation="com.intellij.ide.util.PsiNavigationSupportImpl"/>
@@ -2339,5 +2339,7 @@ ide.log.coroutine.pce.description=Log PCEs occurring in coroutines. \
ide.colorful.toolbar.force=false
ide.colorful.toolbar.force.description=Show 'Colorful toolbar' even if there is one project open (works only if the feature is enabled). \
Reopen project or restart IDE to apply the setting
ide.navigation.requests=true
ide.navigation.requests.description=When navigation works by computing a navigation request on a BG thread.
# please leave this note as last line
# TODO please use EP com.intellij.registryKey for plugin/product specific keys