diff --git a/plugins/markdown/core/frontend/resources/intellij.markdown.frontend.xml b/plugins/markdown/core/frontend/resources/intellij.markdown.frontend.xml index aa9284e235de..e17a79abc145 100644 --- a/plugins/markdown/core/frontend/resources/intellij.markdown.frontend.xml +++ b/plugins/markdown/core/frontend/resources/intellij.markdown.frontend.xml @@ -4,7 +4,6 @@ - + serviceImplementation="org.intellij.plugins.markdown.frontend.preview.accessor.impl.MarkdownLinkOpenerImpl"/> \ No newline at end of file diff --git a/plugins/markdown/core/frontend/src/org/intellij/plugins/markdown/frontend/preview/accessor/DelegatingMarkdownLinkOpener.kt b/plugins/markdown/core/frontend/src/org/intellij/plugins/markdown/frontend/preview/accessor/DelegatingMarkdownLinkOpener.kt deleted file mode 100644 index cafedaa3fcce..000000000000 --- a/plugins/markdown/core/frontend/src/org/intellij/plugins/markdown/frontend/preview/accessor/DelegatingMarkdownLinkOpener.kt +++ /dev/null @@ -1,35 +0,0 @@ -package org.intellij.plugins.markdown.frontend.preview.accessor - -import com.intellij.openapi.components.service -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.registry.Registry -import com.intellij.openapi.vfs.VirtualFile -import org.intellij.plugins.markdown.ui.preview.accessor.MarkdownLinkOpener -import org.intellij.plugins.markdown.ui.preview.accessor.impl.MarkdownLinkOpenerImpl - -class DelegatingMarkdownLinkOpener : MarkdownLinkOpener { - private val oldImplementation: MarkdownLinkOpenerImpl by lazy { - service() - } - private val newImplementation: org.intellij.plugins.markdown.frontend.preview.accessor.impl.MarkdownLinkOpenerImpl by lazy { - service() - } - - private val useFallbackLinkOpener: Boolean = Registry.`is`("markdown.use.fallback.link.opener", false) - - override fun openLink(project: Project?, link: String, virtualFile: VirtualFile?) { - if (useFallbackLinkOpener) { - oldImplementation.openLink(project, link, virtualFile) - } else { - newImplementation.openLink(project, link, virtualFile) - } - } - - override fun isSafeLink(project: Project?, link: String): Boolean { - return if (useFallbackLinkOpener) { - oldImplementation.isSafeLink(project, link) - } else { - newImplementation.isSafeLink(project, link) - } - } -} \ No newline at end of file diff --git a/plugins/markdown/core/frontend/src/org/intellij/plugins/markdown/frontend/preview/accessor/impl/MarkdownLinkOpenerImpl.kt b/plugins/markdown/core/frontend/src/org/intellij/plugins/markdown/frontend/preview/accessor/impl/MarkdownLinkOpenerImpl.kt index 417b6934670b..9af7fd715eed 100644 --- a/plugins/markdown/core/frontend/src/org/intellij/plugins/markdown/frontend/preview/accessor/impl/MarkdownLinkOpenerImpl.kt +++ b/plugins/markdown/core/frontend/src/org/intellij/plugins/markdown/frontend/preview/accessor/impl/MarkdownLinkOpenerImpl.kt @@ -1,7 +1,6 @@ package org.intellij.plugins.markdown.frontend.preview.accessor.impl import com.intellij.ide.BrowserUtil -import com.intellij.ide.vfs.rpcId import com.intellij.openapi.application.EDT import com.intellij.openapi.application.runReadAction import com.intellij.openapi.diagnostic.logger @@ -16,7 +15,6 @@ import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.openapi.ui.popup.PopupStep import com.intellij.openapi.ui.popup.util.BaseListPopupStep import com.intellij.openapi.util.Disposer -import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.wm.WindowManager import com.intellij.platform.project.projectId import com.intellij.ui.awt.RelativePoint @@ -40,7 +38,7 @@ internal class MarkdownLinkOpenerImpl private val coroutineScope: CoroutineScope ) : MarkdownLinkOpener { - override fun openLink(project: Project?, link: String, virtualFile: VirtualFile?) { + override fun openLink(project: Project?, link: String) { val uri = createUri(link) ?: return if (tryOpenInEditor(project, uri)) { return diff --git a/plugins/markdown/core/resources/META-INF/plugin.xml b/plugins/markdown/core/resources/META-INF/plugin.xml index 0757109f129b..376dcec1855d 100644 --- a/plugins/markdown/core/resources/META-INF/plugin.xml +++ b/plugins/markdown/core/resources/META-INF/plugin.xml @@ -364,10 +364,6 @@ - - diff --git a/plugins/markdown/core/src/org/intellij/plugins/markdown/ui/preview/accessor/impl/MarkdownLinkOpenerImpl.kt b/plugins/markdown/core/src/org/intellij/plugins/markdown/ui/preview/accessor/impl/MarkdownLinkOpenerImpl.kt deleted file mode 100644 index ab18a92441a1..000000000000 --- a/plugins/markdown/core/src/org/intellij/plugins/markdown/ui/preview/accessor/impl/MarkdownLinkOpenerImpl.kt +++ /dev/null @@ -1,272 +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 org.intellij.plugins.markdown.ui.preview.accessor.impl - -import com.intellij.ide.BrowserUtil -import com.intellij.ide.actions.OpenFileAction -import com.intellij.openapi.application.invokeLater -import com.intellij.openapi.application.runReadAction -import com.intellij.openapi.components.Service -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.fileEditor.FileDocumentManager -import com.intellij.openapi.fileEditor.FileEditorManager -import com.intellij.openapi.fileEditor.OpenFileDescriptor -import com.intellij.openapi.project.DumbModeBlockedFunctionality -import com.intellij.openapi.project.DumbService -import com.intellij.openapi.project.Project -import com.intellij.openapi.project.guessProjectForFile -import com.intellij.openapi.ui.DoNotAskOption -import com.intellij.openapi.ui.MessageDialogBuilder -import com.intellij.openapi.ui.MessageType -import com.intellij.openapi.ui.popup.Balloon -import com.intellij.openapi.ui.popup.JBPopupFactory -import com.intellij.openapi.ui.popup.PopupStep -import com.intellij.openapi.ui.popup.util.BaseListPopupStep -import com.intellij.openapi.util.Disposer -import com.intellij.openapi.util.SystemInfo -import com.intellij.openapi.vfs.VfsUtil -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.openapi.wm.WindowManager -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiManager -import com.intellij.psi.search.GlobalSearchScope -import com.intellij.ui.awt.RelativePoint -import com.intellij.util.PsiNavigateUtil -import com.intellij.util.UriUtil -import com.intellij.util.concurrency.annotations.RequiresEdt -import com.intellij.util.io.isLocalHost -import org.intellij.plugins.markdown.MarkdownBundle -import org.intellij.plugins.markdown.lang.index.HeaderAnchorIndex -import org.intellij.plugins.markdown.settings.DocumentLinksSafeState -import org.intellij.plugins.markdown.ui.MarkdownNotifications -import org.intellij.plugins.markdown.ui.preview.MarkdownEditorWithPreview -import org.intellij.plugins.markdown.ui.preview.accessor.MarkdownLinkOpener -import org.intellij.plugins.markdown.util.MarkdownDisposable -import org.jetbrains.annotations.ApiStatus -import java.net.URI -import java.net.URISyntaxException -import java.nio.file.Path - -@Service -@ApiStatus.Internal -class MarkdownLinkOpenerImpl : MarkdownLinkOpener { - override fun openLink(project: Project?, link: String, virtualFile: VirtualFile?) { - val uri = createUri(link) ?: return - if (tryOpenInEditor(project, uri)) { - return - } - invokeLater { - openExternalLink(project, uri) - } - } - - override fun isSafeLink(project: Project?, link: String): Boolean { - val uri = createUri(link)?: return false - return isSafeUri(project, uri) - } - - private fun isSafeUri(project: Project?, uri: URI): Boolean { - val protocol = uri.scheme ?: return false - if (project != null) { - val safeLinksState = DocumentLinksSafeState.getInstance(project) - return safeLinksState.isProtocolAllowed(protocol) - } - return DocumentLinksSafeState.isHttpScheme(protocol) && isLocalHost(uri.host) - } - - @RequiresEdt - private fun openExternalLink(project: Project?, uri: URI) { - if (isSafeUri(project, uri)) { - actuallyBrowseExternalLink(project, uri) - return - } - if (showDialog(project, uri)) { - actuallyBrowseExternalLink(project, uri) - } - } - - @RequiresEdt - private fun showDialog(project: Project?, uri: URI): Boolean { - val dialog = MessageDialogBuilder.yesNo( - title = MarkdownBundle.message("markdown.browse.external.link.open.confirmation.dialog.title"), - message = MarkdownBundle.message("markdown.browse.external.link.open.confirmation.dialog.text", uri) - ).doNotAsk(createDoNotAskOption(project, uri)) - return dialog.ask(project) - } - - @RequiresEdt - private fun actuallyBrowseExternalLink(project: Project?, uri: URI) { - try { - BrowserUtil.browse(uri) - } catch (exception: Throwable) { - logger.warn("Failed to browse external link!", exception) - MarkdownNotifications.showWarning( - project, - id = "markdown.links.external.open.failed", - title = MarkdownBundle.message("markdown.browse.external.link.failed.notification.title"), - message = MarkdownBundle.message("markdown.browse.external.link.failed.notification.content", uri), - ) - } - } - - private fun createDoNotAskOption(project: Project?, uri: URI): DoNotAskOption? { - if (project == null) { - return null - } - val protocol = uri.scheme - if (protocol == null) { - logger.error("Failed to obtain protocol for link: $uri") - return null - } - return object: DoNotAskOption.Adapter() { - override fun rememberChoice(isSelected: Boolean, exitCode: Int) { - if (isSelected) { - DocumentLinksSafeState.getInstance(project).allowProtocol(protocol) - } - } - - override fun getDoNotShowMessage(): String { - return MarkdownBundle.message("markdown.browse.external.link.open.confirmation.dialog.do.not.ask.again.text", protocol) - } - } - } - - companion object { - private val logger = logger() - - fun createUri(link: String): URI? { - return try { - when { - BrowserUtil.isAbsoluteURL(link) -> URI(link) - else -> URI("http://$link") - } - } catch (exception: URISyntaxException) { - logger.warn(exception) - null - } - } - - private fun isLocalHost(hostName: String?): Boolean { - return hostName == null || - hostName.startsWith("127.") || - hostName.endsWith(":1") || - isLocalHost(hostName, false, false) - } - - private fun tryOpenInEditor(project: Project?, uri: URI): Boolean { - if (uri.scheme != "file") { - return false - } - return runReadAction { - actuallyOpenInEditor(project, uri) - } - } - - 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 actuallyOpenInEditor(project: Project?, uri: URI): Boolean { - val anchor = uri.fragment - val targetFile = uri.findVirtualFile() ?: return false - @Suppress("NAME_SHADOWING") - val project = project ?: guessProjectForFile(targetFile) ?: return false - if (anchor == null) { - invokeLater { - OpenFileAction.openFile(targetFile, project) - } - return true - } - val point = obtainHeadersPopupPosition(project) - if (point == null) { - logger.warn("Failed to obtain screen point for showing popup") - return false - } - val headers = runReadAction { - if (DumbService.isDumb(project)) { - return@runReadAction 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) - } - if (headers == null) { - invokeLater { - 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 - } - invokeLater { - when { - headers.isEmpty() -> showCannotNavigateNotification(project, anchor, point) - headers.size == 1 -> navigateToHeader(project, targetFile, headers.first()) - else -> showHeadersPopup(project, headers, point) - } - } - return true - } - - private fun obtainHeadersPopupPosition(project: Project?): RelativePoint? { - val frame = WindowManager.getInstance().getFrame(project) - val mousePosition = frame?.mousePosition ?: return null - return RelativePoint(frame, mousePosition) - } - - private fun showCannotNavigateNotification(project: Project, anchor: String, point: RelativePoint) { - val balloonBuilder = JBPopupFactory.getInstance().createHtmlTextBalloonBuilder( - MarkdownBundle.message("markdown.navigate.to.header.no.headers", anchor), - MessageType.WARNING, - null - ) - val balloon = balloonBuilder.createBalloon() - Disposer.register(MarkdownDisposable.getInstance(project), balloon) - balloon.show(point, Balloon.Position.below) - } - - private fun showHeadersPopup(project: Project, headers: Collection, point: RelativePoint) { - JBPopupFactory.getInstance().createListPopup(HeadersPopup(project, headers.toList())).show(point) - } - - private class HeadersPopup( - private val project: Project, - headers: List - ): BaseListPopupStep(MarkdownBundle.message("markdown.navigate.to.header"), headers) { - override fun getTextFor(value: PsiElement): String { - val document = FileDocumentManager.getInstance().getDocument(value.containingFile.virtualFile) - requireNotNull(document) - val name = value.containingFile.virtualFile.name - val line = document.getLineNumber(value.textOffset) + 1 - return "${value.text} ($name:$line)" - } - - override fun onChosen(selectedValue: PsiElement, finalChoice: Boolean): PopupStep<*> { - return doFinalStep { - navigateToHeader(project, selectedValue.containingFile.virtualFile, selectedValue) - } - } - } - - private fun navigateToHeader(project: Project, file: VirtualFile, element: PsiElement) { - val manager = FileEditorManager.getInstance(project) - val openedEditors = manager.getEditorList(file).filterIsInstance() - if (openedEditors.isNotEmpty()) { - for (editor in openedEditors) { - PsiNavigateUtil.navigate(element, true) - } - return - } - val descriptor = OpenFileDescriptor(project, file, element.textOffset) - manager.openEditor(descriptor, true) - } - } -}