diff --git a/plugins/markdown/core/backend/intellij.markdown.backend.iml b/plugins/markdown/core/backend/intellij.markdown.backend.iml
index 544f6e410563..ff2081e18ae9 100644
--- a/plugins/markdown/core/backend/intellij.markdown.backend.iml
+++ b/plugins/markdown/core/backend/intellij.markdown.backend.iml
@@ -1,5 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $KOTLIN_BUNDLED$/lib/kotlinx-serialization-compiler-plugin.jar
+ $MAVEN_REPOSITORY$/jetbrains/fleet/rhizomedb-compiler-plugin/2.1.10-0.1/rhizomedb-compiler-plugin-2.1.10-0.1.jar
+ $MAVEN_REPOSITORY$/com/jetbrains/fleet/rpc-compiler-plugin/2.1.10-0.1/rpc-compiler-plugin-2.1.10-0.1.jar
+
+
+
+
+
+
+
+
@@ -16,5 +42,13 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/markdown/core/backend/resources/intellij.markdown.backend.xml b/plugins/markdown/core/backend/resources/intellij.markdown.backend.xml
index 0f2813495ee4..084ed35c13f4 100644
--- a/plugins/markdown/core/backend/resources/intellij.markdown.backend.xml
+++ b/plugins/markdown/core/backend/resources/intellij.markdown.backend.xml
@@ -1,5 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/plugins/markdown/core/backend/src/org/intellij/plugins/markdown/backend/providers/LinkOpenerApiProvider.kt b/plugins/markdown/core/backend/src/org/intellij/plugins/markdown/backend/providers/LinkOpenerApiProvider.kt
new file mode 100644
index 000000000000..9a45f78b8c98
--- /dev/null
+++ b/plugins/markdown/core/backend/src/org/intellij/plugins/markdown/backend/providers/LinkOpenerApiProvider.kt
@@ -0,0 +1,14 @@
+package org.intellij.plugins.markdown.backend.providers
+
+import com.intellij.platform.rpc.backend.RemoteApiProvider
+import fleet.rpc.remoteApiDescriptor
+import org.intellij.plugins.markdown.backend.services.MarkdownLinkOpenerRemoteApiImpl
+import org.intellij.plugins.markdown.service.MarkdownLinkOpenerRemoteApi
+
+private class LinkOpenerApiProvider : RemoteApiProvider {
+ override fun RemoteApiProvider.Sink.remoteApis() {
+ remoteApi(remoteApiDescriptor()) {
+ MarkdownLinkOpenerRemoteApiImpl()
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/markdown/core/backend/src/org/intellij/plugins/markdown/backend/services/MarkdownLinkOpenerRemoteApiImpl.kt b/plugins/markdown/core/backend/src/org/intellij/plugins/markdown/backend/services/MarkdownLinkOpenerRemoteApiImpl.kt
new file mode 100644
index 000000000000..eba4115f6a35
--- /dev/null
+++ b/plugins/markdown/core/backend/src/org/intellij/plugins/markdown/backend/services/MarkdownLinkOpenerRemoteApiImpl.kt
@@ -0,0 +1,114 @@
+// 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.backend.services
+
+import com.intellij.ide.actions.OpenFileAction
+import com.intellij.openapi.application.runReadAction
+import com.intellij.openapi.diagnostic.Logger
+import com.intellij.openapi.fileEditor.FileEditorManager
+import com.intellij.openapi.fileEditor.OpenFileDescriptor
+import com.intellij.openapi.project.DumbService
+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.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.psi.util.PsiUtilCore
+import com.intellij.util.PsiNavigateUtil
+import com.intellij.util.UriUtil
+import org.intellij.plugins.markdown.dto.MarkdownHeaderInfo
+import org.intellij.plugins.markdown.lang.index.HeaderAnchorIndex
+import org.intellij.plugins.markdown.mapper.MarkdownHeaderMapper
+import org.intellij.plugins.markdown.service.MarkdownLinkOpenerRemoteApi
+import org.intellij.plugins.markdown.ui.preview.MarkdownEditorWithPreview
+import java.net.URI
+import java.net.URISyntaxException
+import java.nio.file.Path
+
+internal class MarkdownLinkOpenerRemoteApiImpl : MarkdownLinkOpenerRemoteApi {
+ companion object {
+ private val logger: Logger = Logger.getInstance(MarkdownLinkOpenerRemoteApiImpl::class.java)
+ }
+
+ override suspend fun openFile(projectId: ProjectId, uri: String) {
+ val project = projectId.findProject()
+ val parsedUri = parseUri(uri)?: return
+ OpenFileAction.openFile(parsedUri.path, project)
+ }
+
+ private fun parseUri(uri: String): URI? {
+ return try {
+ URI(uri)
+ } catch (e: URISyntaxException) {
+ logger.warn(e)
+ return null
+ }
+ }
+
+ override suspend fun collectHeaders(projectId: ProjectId?, uri: String): List {
+ val project = projectId?.findProject() ?: return emptyList()
+ val parsedUri = parseUri(uri)?: return emptyList()
+ val targetFile = parsedUri.findVirtualFile() ?: return emptyList()
+ val anchor = parsedUri.fragment
+
+ return runReadAction {
+ if (DumbService.isDumb(project)) {
+ return@runReadAction emptyList()
+ }
+ val scope = when (val file = PsiManager.getInstance(project).findFile(targetFile)) {
+ null -> GlobalSearchScope.EMPTY_SCOPE
+ else -> GlobalSearchScope.fileScope(file)
+ }
+ 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()
+ }
+
+ override suspend fun navigateToHeader(projectId: ProjectId, headerInfo: MarkdownHeaderInfo) {
+ val uri = createFileUri(headerInfo.filePath) ?: return
+ val file = uri.findVirtualFile() ?: return
+ val project = projectId.findProject()
+ val manager = FileEditorManager.getInstance(project)
+ val openedEditors = manager.getEditorList(file).filterIsInstance()
+ val psiFile = PsiUtilCore.getPsiFile(project, file)
+ val element = psiFile.findElementAt(headerInfo.textOffset) ?: return
+ if (openedEditors.isNotEmpty()) {
+ openedEditors.forEach {
+ // Ensures the element is located as in the original implementation
+ PsiUtilCore.getElementAtOffset(psiFile, element.textOffset)
+ PsiNavigateUtil.navigate(element, true)
+ }
+ return
+ }
+ val descriptor = OpenFileDescriptor(project, file, element.textOffset)
+ manager.openEditor(descriptor, true)
+ }
+
+ 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 createFileUri(link: String): URI? {
+ return try {
+ URI("file", null, link, null)
+ } catch (e: URISyntaxException) {
+ logger.warn(e)
+ null
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/markdown/core/frontend/intellij.markdown.frontend.iml b/plugins/markdown/core/frontend/intellij.markdown.frontend.iml
index e9e81b98116e..c8740bf78756 100644
--- a/plugins/markdown/core/frontend/intellij.markdown.frontend.iml
+++ b/plugins/markdown/core/frontend/intellij.markdown.frontend.iml
@@ -1,5 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $KOTLIN_BUNDLED$/lib/kotlinx-serialization-compiler-plugin.jar
+ $MAVEN_REPOSITORY$/jetbrains/fleet/rhizomedb-compiler-plugin/2.1.10-0.1/rhizomedb-compiler-plugin-2.1.10-0.1.jar
+ $MAVEN_REPOSITORY$/com/jetbrains/fleet/rpc-compiler-plugin/2.1.10-0.1/rpc-compiler-plugin-2.1.10-0.1.jar
+
+
+
+
+
+
+
+
@@ -15,5 +41,6 @@
+
\ No newline at end of file
diff --git a/plugins/markdown/core/intellij.markdown.iml b/plugins/markdown/core/intellij.markdown.iml
index 6ab959c1912f..7baa00073b8b 100644
--- a/plugins/markdown/core/intellij.markdown.iml
+++ b/plugins/markdown/core/intellij.markdown.iml
@@ -1,5 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $KOTLIN_BUNDLED$/lib/kotlinx-serialization-compiler-plugin.jar
+ $MAVEN_REPOSITORY$/jetbrains/fleet/rhizomedb-compiler-plugin/2.1.10-0.1/rhizomedb-compiler-plugin-2.1.10-0.1.jar
+ $MAVEN_REPOSITORY$/com/jetbrains/fleet/rpc-compiler-plugin/2.1.10-0.1/rpc-compiler-plugin-2.1.10-0.1.jar
+
+
+
+
+
+
+
+
@@ -47,5 +73,6 @@
+
diff --git a/plugins/markdown/core/src/org/intellij/plugins/markdown/service/MarkdownLinkOpenerRemoteApi.kt b/plugins/markdown/core/src/org/intellij/plugins/markdown/service/MarkdownLinkOpenerRemoteApi.kt
new file mode 100644
index 000000000000..e29faf138898
--- /dev/null
+++ b/plugins/markdown/core/src/org/intellij/plugins/markdown/service/MarkdownLinkOpenerRemoteApi.kt
@@ -0,0 +1,26 @@
+// 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.service
+
+import com.intellij.platform.project.ProjectId
+import com.intellij.platform.rpc.RemoteApiProviderService
+import fleet.rpc.RemoteApi
+import fleet.rpc.Rpc
+import fleet.rpc.remoteApiDescriptor
+import org.intellij.plugins.markdown.dto.MarkdownHeaderInfo
+import org.jetbrains.annotations.ApiStatus
+
+@ApiStatus.Internal
+@Rpc
+interface MarkdownLinkOpenerRemoteApi: RemoteApi {
+ suspend fun openFile(projectId: ProjectId, uri: String)
+ suspend fun collectHeaders(projectId: ProjectId?, uri: String): List
+ suspend fun guessProjectForUri(uri: String): ProjectId?
+ suspend fun navigateToHeader(projectId: ProjectId, headerInfo: MarkdownHeaderInfo)
+
+ companion object {
+ @JvmStatic
+ suspend fun getInstance(): MarkdownLinkOpenerRemoteApi {
+ return RemoteApiProviderService.resolve(remoteApiDescriptor())
+ }
+ }
+}
\ No newline at end of file