mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[markdown] IJPL-177111 combine multiple RPCs needed for link opening into one
Refactored Markdown link navigation by introducing MarkdownLinkNavigationData. Legacy URI-based handling has been replaced to streamline file and header navigation while eliminating redundant methods. Instead of performing slow findVirtualFile calls on EDT, the implementation now passes a VirtualFile directly to the openFile and navigateToHeader actions. Related services, DTOs, and mappings were updated accordingly to support the new, more efficient approach. GitOrigin-RevId: 4545b23b17c55fe191e1b3c1f77617b6c409818d
This commit is contained in:
committed by
intellij-monorepo-bot
parent
4ba841da7b
commit
8cd04eb35c
+58
-48
@@ -2,23 +2,25 @@
|
||||
// Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.intellij.plugins.markdown.backend.services
|
||||
|
||||
import com.intellij.ide.BrowserUtil
|
||||
import com.intellij.ide.vfs.VirtualFileId
|
||||
import com.intellij.ide.vfs.rpcId
|
||||
import com.intellij.ide.vfs.virtualFile
|
||||
import com.intellij.openapi.application.runReadAction
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.project.DumbService
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.guessProjectForFile
|
||||
import com.intellij.openapi.util.SystemInfo
|
||||
import com.intellij.openapi.vfs.VfsUtil
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.openapi.vfs.findFile
|
||||
import com.intellij.platform.project.ProjectId
|
||||
import com.intellij.platform.project.findProject
|
||||
import com.intellij.platform.project.projectId
|
||||
import com.intellij.psi.PsiManager
|
||||
import com.intellij.psi.search.GlobalSearchScope
|
||||
import com.intellij.util.UriUtil
|
||||
import org.intellij.plugins.markdown.dto.MarkdownHeaderInfo
|
||||
import org.intellij.plugins.markdown.dto.MarkdownLinkNavigationData
|
||||
import org.intellij.plugins.markdown.lang.index.HeaderAnchorIndex
|
||||
import org.intellij.plugins.markdown.mapper.MarkdownHeaderMapper
|
||||
import org.intellij.plugins.markdown.service.MarkdownLinkOpenerRemoteApi
|
||||
@@ -29,43 +31,67 @@ import java.nio.file.Path
|
||||
internal class MarkdownLinkOpenerRemoteApiImpl : MarkdownLinkOpenerRemoteApi {
|
||||
companion object {
|
||||
private val logger: Logger = Logger.getInstance(MarkdownLinkOpenerRemoteApiImpl::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to resolve the link as a path to file. Path to file can be:
|
||||
* - absolute path
|
||||
* - relative path to the file from which the link is resolved
|
||||
* @param link the link to resolve
|
||||
* @param virtualFileId the id of the file from which the link is resolved
|
||||
* @return the path to the file as schema if the link is resolved successfully, null otherwise
|
||||
*/
|
||||
override suspend fun resolveLinkAsFilePath(link: String, virtualFileId: VirtualFileId?): String?{
|
||||
val containingFile = virtualFileId?.virtualFile()?.parent ?: return null
|
||||
val targetFile = containingFile.findFile(link.trimAnchor()) ?: return null
|
||||
val anchor = if ('#' in link) link.substring(link.lastIndexOf('#')) else ""
|
||||
return targetFile.url + anchor
|
||||
}
|
||||
private fun extractAnchor(link: String): String {
|
||||
val lastHashIndex = link.lastIndexOf('#')
|
||||
if (lastHashIndex == -1) {
|
||||
return ""
|
||||
}
|
||||
val potentialAnchor = link.substring(lastHashIndex + 1)
|
||||
if (potentialAnchor.contains("/") || potentialAnchor.contains("\\")) {
|
||||
return ""
|
||||
}
|
||||
return potentialAnchor
|
||||
}
|
||||
|
||||
private fun String.trimAnchor(): String {
|
||||
val anchorIndex = lastIndexOf('#')
|
||||
return if (anchorIndex == -1) this else substring(0, anchorIndex)
|
||||
}
|
||||
private fun String.trimAnchor(): String {
|
||||
val anchorIndex = lastIndexOf('#')
|
||||
return if (anchorIndex == -1) this else substring(0, anchorIndex)
|
||||
}
|
||||
|
||||
private fun parseUri(uri: String): URI? {
|
||||
return try {
|
||||
URI(uri)
|
||||
} catch (e: URISyntaxException) {
|
||||
logger.warn(e)
|
||||
return null
|
||||
private fun URI.findVirtualFile(): VirtualFile? {
|
||||
val actualPath = when {
|
||||
SystemInfo.isWindows -> UriUtil.trimLeadingSlashes(path)
|
||||
else -> path
|
||||
}
|
||||
val path = Path.of(actualPath)
|
||||
return VfsUtil.findFile(path, true)
|
||||
}
|
||||
|
||||
private fun createUri(link: String): URI? {
|
||||
return try {
|
||||
URI(link)
|
||||
} catch (exception: URISyntaxException) {
|
||||
logger.warn(exception)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun collectHeaders(projectId: ProjectId?, uri: String): List<MarkdownHeaderInfo>? {
|
||||
val project = projectId?.findProject() ?: return emptyList()
|
||||
val parsedUri = parseUri(uri)?: return emptyList()
|
||||
val targetFile = parsedUri.findVirtualFile() ?: return emptyList()
|
||||
val anchor = parsedUri.fragment
|
||||
override suspend fun fetchLinkNavigationData(link: String, virtualFileId: VirtualFileId?): MarkdownLinkNavigationData {
|
||||
val file = resolveLinkAsFile(link, virtualFileId)?: return MarkdownLinkNavigationData(link, null, null, null)
|
||||
var path = file.url
|
||||
val anchor = extractAnchor(link)
|
||||
if (!anchor.isEmpty()) path += "#$anchor"
|
||||
val project = guessProjectForFile(file)?: return MarkdownLinkNavigationData(path, file.rpcId(), null, null)
|
||||
if (anchor.isEmpty()) return MarkdownLinkNavigationData(path, file.rpcId(), project.projectId(), null)
|
||||
val headers = collectHeaders(anchor, file, project)
|
||||
return MarkdownLinkNavigationData(path, file.rpcId(), project.projectId(), headers)
|
||||
}
|
||||
|
||||
private fun resolveLinkAsFile(link: String, virtualFileId: VirtualFileId?): VirtualFile?{
|
||||
if (BrowserUtil.isAbsoluteURL(link)){
|
||||
val uri = createUri(link)
|
||||
if (uri != null && uri.scheme == "file") {
|
||||
return uri.findVirtualFile()
|
||||
}
|
||||
}
|
||||
val containingFile = virtualFileId?.virtualFile()?.parent ?: return null
|
||||
val targetFile = containingFile.findFile(link.trimAnchor()) ?: return null
|
||||
return targetFile
|
||||
}
|
||||
|
||||
private fun collectHeaders(anchor: String, targetFile: VirtualFile, project: Project): List<MarkdownHeaderInfo>? {
|
||||
return runReadAction {
|
||||
if (DumbService.isDumb(project)) {
|
||||
return@runReadAction emptyList()
|
||||
@@ -77,20 +103,4 @@ internal class MarkdownLinkOpenerRemoteApiImpl : MarkdownLinkOpenerRemoteApi {
|
||||
return@runReadAction HeaderAnchorIndex.collectHeaders(project, scope, anchor).map(MarkdownHeaderMapper::map)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun guessProjectForUri(uri: String): ProjectId? {
|
||||
val parsedUri = parseUri(uri) ?: return null
|
||||
val file = parsedUri.findVirtualFile() ?: return null
|
||||
val project = guessProjectForFile(file) ?: return null
|
||||
return project.projectId()
|
||||
}
|
||||
|
||||
private fun URI.findVirtualFile(): VirtualFile? {
|
||||
val actualPath = when {
|
||||
SystemInfo.isWindows -> UriUtil.trimLeadingSlashes(path)
|
||||
else -> path
|
||||
}
|
||||
val path = Path.of(actualPath)
|
||||
return VfsUtil.findFile(path, true)
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
<applicationService serviceInterface="org.intellij.plugins.markdown.ui.preview.accessor.MarkdownLinkOpener"
|
||||
serviceImplementation="org.intellij.plugins.markdown.frontend.preview.accessor.impl.MarkdownLinkOpenerImpl"/>
|
||||
<registryKey key="markdown.open.link.fallback"
|
||||
defaultValue="true"
|
||||
defaultValue="false"
|
||||
description="Should fallback to Markdown link opening implementation before the split."/>
|
||||
</extensions>
|
||||
</idea-plugin>
|
||||
+46
-89
@@ -3,10 +3,10 @@ package org.intellij.plugins.markdown.frontend.preview.accessor.impl
|
||||
import com.intellij.ide.BrowserUtil
|
||||
import com.intellij.ide.actions.OpenFileAction
|
||||
import com.intellij.ide.vfs.rpcId
|
||||
import com.intellij.ide.vfs.virtualFile
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.application.runReadAction
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.progress.runBlockingCancellable
|
||||
import com.intellij.openapi.project.DumbModeBlockedFunctionality
|
||||
import com.intellij.openapi.project.DumbService
|
||||
import com.intellij.openapi.project.Project
|
||||
@@ -23,7 +23,6 @@ import com.intellij.openapi.wm.WindowManager
|
||||
import com.intellij.platform.project.findProject
|
||||
import com.intellij.platform.project.projectId
|
||||
import com.intellij.ui.awt.RelativePoint
|
||||
import com.intellij.util.PlatformUtils
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.io.isLocalHost
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -53,12 +52,53 @@ internal class MarkdownLinkOpenerImpl(val coroutineScope: CoroutineScope) : Mark
|
||||
}
|
||||
|
||||
override fun openLink(project: Project?, link: String, containingFile: VirtualFile?) {
|
||||
val uri = createUri(link, containingFile) ?: return
|
||||
if (tryOpenInEditor(project, uri)) {
|
||||
coroutineScope.launch {
|
||||
val data = MarkdownLinkOpenerRemoteApi.getInstance().fetchLinkNavigationData(link, containingFile?.rpcId())
|
||||
val uri = createUri(data.uri) ?: return@launch
|
||||
if (uri.scheme != "file") {
|
||||
openExternalLink(project, uri)
|
||||
return@launch
|
||||
}
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val project = project ?: data.projectId?.findProject() ?: return@launch
|
||||
val fileToOpen = data.virtualFileId?.virtualFile() ?: return@launch
|
||||
val anchor = uri.fragment
|
||||
if (anchor == null) {
|
||||
withContext(Dispatchers.EDT) {
|
||||
runReadAction {
|
||||
OpenFileAction.openFile(fileToOpen, project)
|
||||
}
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
processHeaders(fileToOpen, anchor, project, data.headers)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun processHeaders(file: VirtualFile, anchor: String, project: Project, headers: List<MarkdownHeaderInfo>?){
|
||||
if (headers == null) {
|
||||
DumbService.getInstance(project).showDumbModeNotificationForFunctionality(
|
||||
message = MarkdownBundle.message("markdown.dumb.mode.navigation.is.not.available.notification.text"),
|
||||
functionality = DumbModeBlockedFunctionality.ActionWithoutId
|
||||
)
|
||||
return
|
||||
}
|
||||
coroutineScope.launch {
|
||||
openExternalLink(project, uri)
|
||||
if (headers.size == 1) {
|
||||
withContext(Dispatchers.EDT) {
|
||||
runReadAction {
|
||||
MarkdownFrontendService.getInstance().navigateToHeader(project.projectId(), headers.first())
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
val point = obtainHeadersPopupPosition(project)
|
||||
if (point == null) {
|
||||
logger.warn("Failed to obtain screen point for showing popup")
|
||||
return
|
||||
}
|
||||
when {
|
||||
headers.isEmpty() -> showCannotNavigateNotification(project, anchor, point)
|
||||
headers.size > 1 -> showHeadersPopup(project, headers, point)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,15 +173,6 @@ internal class MarkdownLinkOpenerImpl(val coroutineScope: CoroutineScope) : Mark
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryOpenInEditor(project: Project?, uri: URI): Boolean {
|
||||
if (uri.scheme != "file") {
|
||||
return false
|
||||
}
|
||||
return runReadAction {
|
||||
actuallyOpenInEditor(project, uri)
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryOpenInEditorDeprecated(project: Project?, uri: URI): Boolean {
|
||||
if (uri.scheme != "file") {
|
||||
return false
|
||||
@@ -193,80 +224,6 @@ internal class MarkdownLinkOpenerImpl(val coroutineScope: CoroutineScope) : Mark
|
||||
return true
|
||||
}
|
||||
|
||||
private fun actuallyOpenInEditor(project: Project?, uri: URI): Boolean {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val project = project ?:
|
||||
runBlockingCancellable {
|
||||
withContext(Dispatchers.IO) {
|
||||
MarkdownLinkOpenerRemoteApi.getInstance().guessProjectForUri(uri.toString())?.findProject()
|
||||
}
|
||||
}
|
||||
if (project == null) return false
|
||||
val anchor = uri.fragment
|
||||
if (anchor == null){
|
||||
coroutineScope.launch(Dispatchers.EDT) {
|
||||
OpenFileAction.openFile(uri.path, project)
|
||||
}
|
||||
return true
|
||||
}
|
||||
var headers = runBlockingCancellable {
|
||||
withContext(Dispatchers.IO) {
|
||||
MarkdownLinkOpenerRemoteApi.getInstance().collectHeaders(project.projectId(), uri.toString())
|
||||
}
|
||||
}
|
||||
if (headers == null) {
|
||||
coroutineScope.launch {
|
||||
DumbService.getInstance(project).showDumbModeNotificationForFunctionality(
|
||||
message = MarkdownBundle.message("markdown.dumb.mode.navigation.is.not.available.notification.text"),
|
||||
functionality = DumbModeBlockedFunctionality.ActionWithoutId
|
||||
)
|
||||
}
|
||||
// Return true to prevent external navigation from happening
|
||||
return true
|
||||
}
|
||||
if (headers.size == 1) {
|
||||
coroutineScope.launch(Dispatchers.EDT) {
|
||||
MarkdownFrontendService.getInstance().navigateToHeader(project.projectId(), headers.first())
|
||||
}
|
||||
return true
|
||||
}
|
||||
val point = obtainHeadersPopupPosition(project)
|
||||
if (point == null) {
|
||||
logger.warn("Failed to obtain screen point for showing popup")
|
||||
return false
|
||||
}
|
||||
coroutineScope.launch {
|
||||
when {
|
||||
headers.isEmpty() -> showCannotNavigateNotification(project, anchor, point)
|
||||
headers.size > 1 -> showHeadersPopup(project, headers, point)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun createUri(link: String, containingFile: VirtualFile?): URI? {
|
||||
return try {
|
||||
if (BrowserUtil.isAbsoluteURL(link)) return URI(link)
|
||||
else {
|
||||
if (PlatformUtils.isJetBrainsClient()){
|
||||
val scheme = runBlockingCancellable {
|
||||
withContext(Dispatchers.IO) {
|
||||
MarkdownLinkOpenerRemoteApi.getInstance().resolveLinkAsFilePath(link, containingFile?.rpcId())
|
||||
}
|
||||
}
|
||||
if (scheme != null && scheme.startsWith("file://")) {
|
||||
return URI(scheme)
|
||||
}
|
||||
}
|
||||
|
||||
return URI("http://$link")
|
||||
}
|
||||
} catch (exception: URISyntaxException) {
|
||||
logger.warn(exception)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val logger = logger<MarkdownLinkOpenerImpl>()
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.intellij.plugins.markdown.dto
|
||||
|
||||
import com.intellij.ide.vfs.VirtualFileId
|
||||
import com.intellij.openapi.util.NlsContexts
|
||||
import com.intellij.platform.project.ProjectId
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.SerialName
|
||||
|
||||
@@ -16,5 +18,19 @@ data class MarkdownHeaderInfo(
|
||||
@SerialName("line_number")
|
||||
val lineNumber: Int,
|
||||
@SerialName("text_offset")
|
||||
val textOffset: Int
|
||||
val textOffset: Int,
|
||||
@SerialName("file_id")
|
||||
val virtualFileId: VirtualFileId
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MarkdownLinkNavigationData(
|
||||
@SerialName("uri")
|
||||
val uri: String,
|
||||
@SerialName("file")
|
||||
val virtualFileId: VirtualFileId?,
|
||||
@SerialName("project")
|
||||
val projectId: ProjectId?,
|
||||
@SerialName("headers")
|
||||
val headers: List<MarkdownHeaderInfo>?
|
||||
)
|
||||
+5
-1
@@ -1,11 +1,13 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.intellij.plugins.markdown.mapper;
|
||||
|
||||
import com.intellij.ide.vfs.VirtualFileId;
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.psi.PsiFile;
|
||||
import org.intellij.plugins.markdown.dto.MarkdownHeaderInfo;
|
||||
import org.intellij.plugins.markdown.lang.psi.impl.MarkdownHeader;
|
||||
import com.intellij.ide.vfs.VirtualFileIdKt;
|
||||
|
||||
/**
|
||||
* Mapper class for converting a MarkdownHeader into a MarkdownHeaderInfo.
|
||||
@@ -36,7 +38,9 @@ public final class MarkdownHeaderMapper {
|
||||
lineNumber = document.getLineNumber(textOffset) + 1;
|
||||
}
|
||||
|
||||
return new MarkdownHeaderInfo(headerText, filePath, fileName, lineNumber, textOffset);
|
||||
VirtualFileId virtualFileId = VirtualFileIdKt.rpcId(header.getContainingFile().getVirtualFile());
|
||||
|
||||
return new MarkdownHeaderInfo(headerText, filePath, fileName, lineNumber, textOffset, virtualFileId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -2,6 +2,7 @@
|
||||
package org.intellij.plugins.markdown.service;
|
||||
|
||||
import com.intellij.ide.actions.OpenFileAction;
|
||||
import com.intellij.ide.vfs.VirtualFileIdKt;
|
||||
import com.intellij.openapi.application.ReadAction;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.fileEditor.FileEditorManager;
|
||||
@@ -70,14 +71,15 @@ public final class MarkdownFrontendServiceImpl implements MarkdownFrontendServic
|
||||
public void navigateToHeader(ProjectId projectId, MarkdownHeaderInfo headerInfo) {
|
||||
URI uri = createFileUri(headerInfo.getFilePath());
|
||||
if (uri == null) return;
|
||||
VirtualFile file = findVirtualFile(uri);
|
||||
VirtualFile file = VirtualFileIdKt.virtualFile(headerInfo.getVirtualFileId());
|
||||
if (file == null) return;
|
||||
Project project = ProjectIdKt.findProject(projectId);
|
||||
FileEditorManager manager = FileEditorManager.getInstance(project);
|
||||
List<MarkdownEditorWithPreview> openedEditors = manager.getEditorList(file).stream()
|
||||
.filter(editor -> editor instanceof MarkdownEditorWithPreview)
|
||||
.map(editor -> (MarkdownEditorWithPreview)editor)
|
||||
.toList();
|
||||
PsiElement element = PsiUtilCore.getPsiFile(project,file).findElementAt(headerInfo.getTextOffset());
|
||||
PsiElement element = PsiUtilCore.getPsiFile(project, file).findElementAt(headerInfo.getTextOffset());
|
||||
if (element == null) return;
|
||||
if (!openedEditors.isEmpty()) {
|
||||
for (MarkdownEditorWithPreview editor : openedEditors) {
|
||||
|
||||
+2
-3
@@ -8,14 +8,13 @@ import fleet.rpc.RemoteApi
|
||||
import fleet.rpc.Rpc
|
||||
import fleet.rpc.remoteApiDescriptor
|
||||
import org.intellij.plugins.markdown.dto.MarkdownHeaderInfo
|
||||
import org.intellij.plugins.markdown.dto.MarkdownLinkNavigationData
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
|
||||
@ApiStatus.Internal
|
||||
@Rpc
|
||||
interface MarkdownLinkOpenerRemoteApi: RemoteApi<Unit> {
|
||||
suspend fun collectHeaders(projectId: ProjectId?, uri: String): List<MarkdownHeaderInfo>?
|
||||
suspend fun guessProjectForUri(uri: String): ProjectId?
|
||||
suspend fun resolveLinkAsFilePath(link: String, virtualFileId: VirtualFileId?): String?
|
||||
suspend fun fetchLinkNavigationData(link: String, virtualFileId: VirtualFileId?): MarkdownLinkNavigationData
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
|
||||
Reference in New Issue
Block a user