Revert "[markdown] IJPL-177111 add a Registry flag for falling back to the old implementation of link opening"

This reverts commit e17374dd72c71d821ba15b2756a13d001d46150c.

GitOrigin-RevId: 748622f699497dc41c35e9676d57928f87f38dc5
This commit is contained in:
Milan Doslic
2025-04-15 07:21:52 +00:00
committed by intellij-monorepo-bot
parent 80847857de
commit d5dd6ad7de
5 changed files with 2 additions and 316 deletions
@@ -4,7 +4,6 @@
</dependencies>
<extensions defaultExtensionNs="com.intellij">
<applicationService serviceInterface="org.intellij.plugins.markdown.ui.preview.accessor.MarkdownLinkOpener"
serviceImplementation="org.intellij.plugins.markdown.frontend.preview.accessor.DelegatingMarkdownLinkOpener"/>
<applicationService serviceImplementation="org.intellij.plugins.markdown.frontend.preview.accessor.impl.MarkdownLinkOpenerImpl"/>
serviceImplementation="org.intellij.plugins.markdown.frontend.preview.accessor.impl.MarkdownLinkOpenerImpl"/>
</extensions>
</idea-plugin>
@@ -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<MarkdownLinkOpenerImpl>()
}
private val newImplementation: org.intellij.plugins.markdown.frontend.preview.accessor.impl.MarkdownLinkOpenerImpl by lazy {
service<org.intellij.plugins.markdown.frontend.preview.accessor.impl.MarkdownLinkOpenerImpl>()
}
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)
}
}
}
@@ -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
@@ -364,10 +364,6 @@
<registryKey key="markdown.experimental.allow.external.requests"
defaultValue="true"
description="Enables requests for external resources from the preview (e.g. non-local images)."/>
<registryKey key="markdown.use.fallback.link.opener"
defaultValue="false"
description="Use the old implementation of the link opener."/>
<!--endregion-->
<advancedSetting id="markdown.hide.floating.toolbar" default="false" groupKey="markdown.settings.name"/>
@@ -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<MarkdownLinkOpenerImpl>()
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<PsiElement>, point: RelativePoint) {
JBPopupFactory.getInstance().createListPopup(HeadersPopup(project, headers.toList())).show(point)
}
private class HeadersPopup(
private val project: Project,
headers: List<PsiElement>
): BaseListPopupStep<PsiElement>(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<MarkdownEditorWithPreview>()
if (openedEditors.isNotEmpty()) {
for (editor in openedEditors) {
PsiNavigateUtil.navigate(element, true)
}
return
}
val descriptor = OpenFileDescriptor(project, file, element.textOffset)
manager.openEditor(descriptor, true)
}
}
}