[markdown] IJPL-177111 refactor Markdown components for consistency and clarity

GitOrigin-RevId: 531f7c07d9a8be35cf6db5bd56a7aaa66a07488c
This commit is contained in:
Milan Doslic
2025-04-15 07:21:53 +00:00
committed by intellij-monorepo-bot
parent be919d5f08
commit 447cea89c5
11 changed files with 104 additions and 104 deletions
@@ -5,8 +5,8 @@
</dependencies>
<extensions defaultExtensionNs="com.intellij">
<platform.rpc.backend.remoteApiProvider
implementation="org.intellij.plugins.markdown.backend.providers.LinkOpenerApiProvider"/>
implementation="org.intellij.plugins.markdown.backend.providers.MarkdownLinkOpenerApiProvider"/>
<platform.rpc.backend.remoteApiProvider
implementation="org.intellij.plugins.markdown.backend.providers.ProjectStructureProvider"/>
implementation="org.intellij.plugins.markdown.backend.providers.VirtualFileAccessorProvider"/>
</extensions>
</idea-plugin>
@@ -5,7 +5,7 @@ import fleet.rpc.remoteApiDescriptor
import org.intellij.plugins.markdown.backend.services.MarkdownLinkOpenerRemoteApiImpl
import org.intellij.plugins.markdown.service.MarkdownLinkOpenerRemoteApi
private class LinkOpenerApiProvider : RemoteApiProvider {
private class MarkdownLinkOpenerApiProvider : RemoteApiProvider {
override fun RemoteApiProvider.Sink.remoteApis() {
remoteApi(remoteApiDescriptor<MarkdownLinkOpenerRemoteApi>()) {
MarkdownLinkOpenerRemoteApiImpl()
@@ -1,14 +0,0 @@
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.ProjectStructureRemoteApiImpl
import org.intellij.plugins.markdown.service.ProjectStructureRemoteApi
private class ProjectStructureProvider : RemoteApiProvider {
override fun RemoteApiProvider.Sink.remoteApis() {
remoteApi(remoteApiDescriptor<ProjectStructureRemoteApi>()) {
ProjectStructureRemoteApiImpl()
}
}
}
@@ -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.VirtualFileAccessorImpl
import org.intellij.plugins.markdown.service.VirtualFileAccessor
private class VirtualFileAccessorProvider : RemoteApiProvider {
override fun RemoteApiProvider.Sink.remoteApis() {
remoteApi(remoteApiDescriptor<VirtualFileAccessor>()) {
VirtualFileAccessorImpl()
}
}
}
@@ -9,11 +9,11 @@ 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 org.intellij.plugins.markdown.service.ProjectStructureRemoteApi
import org.intellij.plugins.markdown.service.VirtualFileAccessor
import java.io.File
import java.net.URL
class ProjectStructureRemoteApiImpl : ProjectStructureRemoteApi {
class VirtualFileAccessorImpl : VirtualFileAccessor {
private fun getBaseDirectory(projectId: ProjectId, virtualFileId: VirtualFileId) : VirtualFile?{
val project = projectId.findProject()
val virtualFile = virtualFileId.virtualFile() ?: return null
@@ -21,8 +21,7 @@ class ProjectStructureRemoteApiImpl : ProjectStructureRemoteApi {
return baseDirectory
}
override suspend fun getFileByResourceName(resourceName: String, virtualFileId: VirtualFileId?, projectId: ProjectId?): VirtualFileId? {
if (projectId == null || virtualFileId == null) return null
override suspend fun getFileByResourceName(resourceName: String, virtualFileId: VirtualFileId, projectId: ProjectId): VirtualFileId? {
val projectRoot = getBaseDirectory(projectId, virtualFileId)
val resource = if (resourceName.startsWith("file:/")) {
VfsUtil.findFileByIoFile(File(URL(resourceName).path), true)
@@ -33,6 +32,6 @@ class ProjectStructureRemoteApiImpl : ProjectStructureRemoteApi {
}
companion object {
private val logger: Logger = Logger.getInstance(ProjectStructureRemoteApiImpl::class.java)
private val logger: Logger = Logger.getInstance(VirtualFileAccessorImpl::class.java)
}
}
@@ -36,12 +36,13 @@ import org.intellij.plugins.markdown.settings.DocumentLinksSafeState
import org.intellij.plugins.markdown.ui.MarkdownNotifications
import org.intellij.plugins.markdown.ui.preview.accessor.MarkdownLinkOpener
import org.intellij.plugins.markdown.ui.preview.accessor.MarkdownLinkOpenerUtil
import org.intellij.plugins.markdown.ui.preview.accessor.MarkdownLinkOpenerUtil.Companion.findVirtualFile
import org.intellij.plugins.markdown.ui.preview.accessor.MarkdownLinkOpenerUtil.findVirtualFile
import org.intellij.plugins.markdown.util.MarkdownDisposable
import java.net.URI
import java.net.URISyntaxException
internal class MarkdownLinkOpenerImpl(val coroutineScope: CoroutineScope) : MarkdownLinkOpener {
@Deprecated("Use openLink(project, link, sourceFile) instead", replaceWith = ReplaceWith("openLink(project, link, sourceFile)"))
override fun openLink(project: Project?, link: String) {
val uri = createUri(link) ?: return
if (tryOpenInEditorDeprecated(project, uri)) {
@@ -52,16 +53,15 @@ internal class MarkdownLinkOpenerImpl(val coroutineScope: CoroutineScope) : Mark
}
}
override fun openLink(project: Project?, link: String, containingFile: VirtualFile?) {
override fun openLink(currentProject: Project?, link: String, containingFile: VirtualFile?) {
coroutineScope.launch {
val data = MarkdownLinkOpenerRemoteApi.getInstance().fetchLinkNavigationData(link, containingFile?.rpcId())
val uri = createUri(data.uri) ?: return@launch
if (uri.scheme != "file") {
openExternalLink(project, uri)
openExternalLink(currentProject, uri)
return@launch
}
@Suppress("NAME_SHADOWING")
val project = project ?: data.projectId?.findProject() ?: return@launch
val project = currentProject ?: data.projectId?.findProject() ?: return@launch
val fileToOpen = data.virtualFileId?.virtualFile() ?: return@launch
val anchor = uri.fragment
if (anchor == null) {
@@ -8,14 +8,14 @@ import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.project.projectId
import kotlinx.coroutines.runBlocking
import org.intellij.plugins.markdown.extensions.MarkdownBrowserPreviewExtension
import org.intellij.plugins.markdown.service.ProjectStructureRemoteApi
import org.intellij.plugins.markdown.service.VirtualFileAccessor
import org.intellij.plugins.markdown.ui.preview.MarkdownHtmlPanel
import org.intellij.plugins.markdown.ui.preview.ResourceProvider
internal class ProcessImagesExtension(
private val baseFile: VirtualFile?,
private val project: Project?
): ResourceProvider, MarkdownBrowserPreviewExtension {
private val project: Project?,
) : ResourceProvider, MarkdownBrowserPreviewExtension {
override val resourceProvider: ResourceProvider = this
override fun canProvide(resourceName: String): Boolean {
@@ -23,32 +23,30 @@ internal class ProcessImagesExtension(
}
override fun loadResource(resourceName: String): ResourceProvider.Resource? {
val baseFileId = baseFile?.rpcId()
val projectId = project?.projectId()
val baseFileId = baseFile?.rpcId() ?: return null
val projectId = project?.projectId() ?: return null
val resource = runBlocking {
ProjectStructureRemoteApi.getInstance().getFileByResourceName(resourceName, baseFileId, projectId)?.virtualFile()
VirtualFileAccessor.getInstance().getFileByResourceName(resourceName, baseFileId, projectId)?.virtualFile()
} ?: return null
return ResourceProvider.loadExternalResource(resource)
}
override fun dispose() = Unit
companion object{
fun hasImageExtension(name: String): Boolean {
val lowerName = name.lowercase()
return lowerName.endsWith(".jpeg") ||
lowerName.endsWith(".jpg") ||
lowerName.endsWith(".png") ||
lowerName.endsWith(".gif") ||
lowerName.endsWith(".bmp") ||
lowerName.endsWith(".svg") ||
lowerName.endsWith(".webp") ||
lowerName.endsWith(".tiff") ||
lowerName.endsWith(".tif")
}
private fun hasImageExtension(name: String): Boolean {
val lowerName = name.lowercase()
return lowerName.endsWith(".jpeg") ||
lowerName.endsWith(".jpg") ||
lowerName.endsWith(".png") ||
lowerName.endsWith(".gif") ||
lowerName.endsWith(".bmp") ||
lowerName.endsWith(".svg") ||
lowerName.endsWith(".webp") ||
lowerName.endsWith(".tiff") ||
lowerName.endsWith(".tif")
}
class Provider: MarkdownBrowserPreviewExtension.Provider {
class Provider : MarkdownBrowserPreviewExtension.Provider {
override fun createBrowserExtension(panel: MarkdownHtmlPanel): MarkdownBrowserPreviewExtension {
return ProcessImagesExtension(panel.virtualFile, panel.project)
}
@@ -2,6 +2,7 @@
package org.intellij.plugins.markdown.mapper;
import com.intellij.ide.vfs.VirtualFileId;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
@@ -13,9 +14,12 @@ import com.intellij.ide.vfs.VirtualFileIdKt;
* Mapper class for converting a MarkdownHeader into a MarkdownHeaderInfo.
*/
public final class MarkdownHeaderMapper {
private static final Logger logger = Logger.getInstance(MarkdownHeaderMapper.class);
public static MarkdownHeaderInfo map(MarkdownHeader header) {
if (header == null) {
throw new IllegalArgumentException("header cannot be null");
logger.warn("Header is null, returning null");
return null;
}
String headerText = header.getText();
@@ -11,13 +11,13 @@ import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
@Rpc
interface ProjectStructureRemoteApi : RemoteApi<Unit> {
suspend fun getFileByResourceName(resourceName: String, virtualFileId: VirtualFileId?, projectId: ProjectId?): VirtualFileId?
interface VirtualFileAccessor : RemoteApi<Unit> {
suspend fun getFileByResourceName(resourceName: String, virtualFileId: VirtualFileId, projectId: ProjectId): VirtualFileId?
companion object {
@JvmStatic
suspend fun getInstance(): ProjectStructureRemoteApi {
return RemoteApiProviderService.resolve(remoteApiDescriptor<ProjectStructureRemoteApi>())
suspend fun getInstance(): VirtualFileAccessor {
return RemoteApiProviderService.resolve(remoteApiDescriptor<VirtualFileAccessor>())
}
}
}
@@ -22,6 +22,7 @@ interface MarkdownLinkOpener {
*
* Note: it is possible to add custom url handler with [com.intellij.ide.browsers.UrlOpener] EP.
*/
@Deprecated("this method lacks remote development support", ReplaceWith("openLink(project, link, sourceFile)"))
fun openLink(project: Project?, link: String)
fun openLink(project: Project?, link: String, sourceFile: VirtualFile?)
@@ -25,63 +25,61 @@ import java.net.URISyntaxException
import java.nio.file.Path
import com.intellij.openapi.diagnostic.logger
class MarkdownLinkOpenerUtil {
companion object{
private val logger = logger<MarkdownLinkOpenerUtil>()
object MarkdownLinkOpenerUtil {
private val logger = logger<MarkdownLinkOpenerUtil>()
fun navigateToHeader(project: Project, headerInfo: MarkdownHeaderInfo) {
val uri = createFileUri(headerInfo.filePath)
if (uri == null) return
val file = headerInfo.virtualFileId.virtualFile()
if (file == null) return
val manager = FileEditorManager.getInstance(project)
val openedEditors = manager.getEditorList(file).stream()
.filter { editor: FileEditor? -> editor is MarkdownEditorWithPreview }
.map<MarkdownEditorWithPreview?> { editor: FileEditor? -> editor as MarkdownEditorWithPreview }
.toList()
val element = PsiUtilCore.getPsiFile(project, file).findElementAt(headerInfo.textOffset)
if (element == null) return
if (!openedEditors.isEmpty()) {
for (editor in openedEditors) {
PsiUtilCore.getElementAtOffset(PsiUtilCore.getPsiFile(project, file), element.getTextOffset())
PsiNavigateUtil.navigate(element, true)
}
return
fun navigateToHeader(project: Project, headerInfo: MarkdownHeaderInfo) {
val uri = createFileUri(headerInfo.filePath)
if (uri == null) return
val file = headerInfo.virtualFileId.virtualFile()
if (file == null) return
val manager = FileEditorManager.getInstance(project)
val openedEditors = manager.getEditorList(file).stream()
.filter { editor: FileEditor? -> editor is MarkdownEditorWithPreview }
.map<MarkdownEditorWithPreview?> { editor: FileEditor? -> editor as MarkdownEditorWithPreview }
.toList()
val element = PsiUtilCore.getPsiFile(project, file).findElementAt(headerInfo.textOffset)
if (element == null) return
if (!openedEditors.isEmpty()) {
for (editor in openedEditors) {
PsiUtilCore.getElementAtOffset(PsiUtilCore.getPsiFile(project, file), element.getTextOffset())
PsiNavigateUtil.navigate(element, true)
}
val descriptor = OpenFileDescriptor(project, file, element.getTextOffset())
manager.openEditor(descriptor, true)
return
}
val descriptor = OpenFileDescriptor(project, file, element.getTextOffset())
manager.openEditor(descriptor, true)
}
fun collectHeaders(project: Project, anchor: String, targetFile: VirtualFile): List<MarkdownHeaderInfo>? {
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)
fun collectHeaders(project: Project, anchor: String, targetFile: VirtualFile): List<MarkdownHeaderInfo>? {
return runReadAction {
if (DumbService.isDumb(project)) {
return@runReadAction emptyList()
}
}
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? {
try {
return URI("file", null, link, null)
}
catch (exception: URISyntaxException) {
logger.warn(exception)
return null
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)
}
}
}
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? {
try {
return URI("file", null, link, null)
}
catch (exception: URISyntaxException) {
logger.warn(exception)
return null
}
}
}