From c9078c27f36c1c38232ee4bba64219802a575552 Mon Sep 17 00:00:00 2001 From: Ivan Migalev Date: Sat, 4 Jan 2025 19:05:03 +0000 Subject: [PATCH] (IJPL-173684, IJ-CR-152618) What's New: separate opening logic for web pages #IJPL-173684 Fixed (cherry picked from commit 46ee26a3a82e70cee2d71dccba8536a058523d05) GitOrigin-RevId: 7309170a8ba2f309cd25128daf1b2842f0dd48a8 --- .../platform/whatsNew/WhatsNewAction.kt | 60 +----- .../platform/whatsNew/WhatsNewContent.kt | 196 +++++++++--------- .../whatsNew/WhatsNewContentVersionChecker.kt | 12 +- .../whatsNew/OnStartCheckServiceTest.kt | 8 +- 4 files changed, 121 insertions(+), 155 deletions(-) diff --git a/platform/whatsNew/src/com/intellij/platform/whatsNew/WhatsNewAction.kt b/platform/whatsNew/src/com/intellij/platform/whatsNew/WhatsNewAction.kt index f7b4b882c74f..e45488fe2789 100644 --- a/platform/whatsNew/src/com/intellij/platform/whatsNew/WhatsNewAction.kt +++ b/platform/whatsNew/src/com/intellij/platform/whatsNew/WhatsNewAction.kt @@ -1,4 +1,4 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.platform.whatsNew import com.intellij.ide.DataManager @@ -9,25 +9,16 @@ import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.application.ApplicationNamesInfo -import com.intellij.openapi.application.EDT -import com.intellij.openapi.application.writeIntentReadAction import com.intellij.openapi.components.Service import com.intellij.openapi.components.service -import com.intellij.openapi.components.serviceAsync import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.diagnostic.runAndLogException -import com.intellij.openapi.fileEditor.FileEditorManager -import com.intellij.openapi.fileEditor.FileEditorManagerListener -import com.intellij.openapi.fileEditor.impl.HTMLEditorProvider.Companion.openEditor import com.intellij.openapi.project.Project -import com.intellij.openapi.util.Disposer -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.platform.whatsNew.collectors.WhatsNewCounterUsageCollector import com.intellij.platform.whatsNew.reaction.FUSReactionChecker -import com.intellij.platform.whatsNew.reaction.ReactionsPanel -import com.intellij.ui.jcef.JBCefApp -import com.intellij.util.application -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.launch import org.jetbrains.concurrency.await internal class WhatsNewAction : AnAction(), com.intellij.openapi.project.DumbAware { @@ -57,44 +48,13 @@ internal class WhatsNewAction : AnAction(), com.intellij.openapi.project.DumbAwa openWhatsNewPage(project, dataContext) } - private suspend fun openWhatsNewPage(project: Project, dataContext: DataContext?, byClient: Boolean = false) { - if (JBCefApp.isSupported()) { - val content = contentAsync.await() - if (content != null && content.isAvailable()) { - openContent(project, content, dataContext, byClient) - } - else { - LOG.warn("EapWhatsNew: can't be shown. Content not available") - } + private suspend fun openWhatsNewPage(project: Project, dataContext: DataContext?, triggeredByUser: Boolean = false) { + val content = contentAsync.await() + if (content != null && content.isAvailable()) { + content.show(project, dataContext, triggeredByUser, reactionChecker) } else { - LOG.warn("EapWhatsNew: can't be shown. JBCefApp isn't supported") - } - } - - private suspend fun openContent(project: Project, whatsNewContent: WhatsNewContent, dataContext: DataContext?, byClient: Boolean) { - check(JBCefApp.isSupported()) { "JCEF is not supported on this system" } - val title = IdeBundle.message("update.whats.new", ApplicationNamesInfo.getInstance().fullProductName) - withContext(Dispatchers.EDT) { - LOG.info("Opening What's New in editor.") - val editor = writeIntentReadAction { openEditor(project, title, whatsNewContent.getRequest(dataContext)) } - editor?.let { - project.serviceAsync().addTopComponent(it, ReactionsPanel.createPanel(PLACE, reactionChecker)) - WhatsNewCounterUsageCollector.openedPerformed(project, byClient) - - WhatsNewContentVersionChecker.saveLastShownContent(whatsNewContent) - - val disposable = Disposer.newDisposable(project) - val busConnection = application.messageBus.connect(disposable) - busConnection.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, object : FileEditorManagerListener { - override fun fileClosed(source: FileEditorManager, file: VirtualFile) { - if (it.file == file) { - WhatsNewCounterUsageCollector.closedPerformed(project) - Disposer.dispose(disposable) - } - } - }) - } + LOG.warn("EapWhatsNew: can't be shown. Content not available") } } diff --git a/platform/whatsNew/src/com/intellij/platform/whatsNew/WhatsNewContent.kt b/platform/whatsNew/src/com/intellij/platform/whatsNew/WhatsNewContent.kt index ebddaf27649f..d8953ec3e9ad 100644 --- a/platform/whatsNew/src/com/intellij/platform/whatsNew/WhatsNewContent.kt +++ b/platform/whatsNew/src/com/intellij/platform/whatsNew/WhatsNewContent.kt @@ -1,37 +1,41 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.platform.whatsNew +import com.intellij.ide.BrowserUtil import com.intellij.ide.IdeBundle import com.intellij.l10n.LocalizationStateService import com.intellij.openapi.actionSystem.* -import com.intellij.openapi.application.ApplicationInfo -import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.* import com.intellij.openapi.components.service +import com.intellij.openapi.components.serviceAsync import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.diagnostic.trace +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.FileEditorManagerListener import com.intellij.openapi.fileEditor.impl.HTMLEditorProvider +import com.intellij.openapi.fileEditor.impl.HTMLEditorProvider.Companion.openEditor import com.intellij.openapi.fileEditor.impl.HTMLEditorProvider.JsQueryHandler import com.intellij.openapi.fileEditor.impl.HTMLEditorProvider.Request.Companion.html -import com.intellij.openapi.fileEditor.impl.HTMLEditorProvider.Request.Companion.url import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.Version +import com.intellij.openapi.vfs.VirtualFile import com.intellij.platform.ide.customization.ExternalProductResourceUrls +import com.intellij.platform.whatsNew.WhatsNewAction.Companion.PLACE import com.intellij.platform.whatsNew.collectors.WhatsNewCounterUsageCollector -import com.intellij.util.Urls.newFromEncoded +import com.intellij.platform.whatsNew.reaction.FUSReactionChecker +import com.intellij.platform.whatsNew.reaction.ReactionsPanel +import com.intellij.ui.jcef.JBCefApp +import com.intellij.util.application import com.intellij.util.io.DigestUtil import com.intellij.util.ui.StartupUiUtil import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import java.io.IOException import java.net.HttpURLConnection import java.net.URL -import java.nio.charset.StandardCharsets internal abstract class WhatsNewContent { companion object { - private val DataContext.project: Project? - get() = CommonDataKeys.PROJECT.getData(this) - suspend fun getWhatsNewContent(): WhatsNewContent? { return if (WhatsNewInVisionContentProvider.getInstance().isAvailable()) { WhatsNewVisionContent(WhatsNewInVisionContentProvider.getInstance().getContent().entities.first()) @@ -41,7 +45,8 @@ internal abstract class WhatsNewContent { } } - // Year and release have to be strings, because this is the ApplicationInfo.xml format. "release" might be a string like "2.1". + // Year and release have to be strings, because this is the ApplicationInfo.xml format. + // Remember that "release" might be a string like "2.1". data class ContentVersion(val year: String, val release: String, val eap: Int?, val hash: String?) : Comparable { companion object { @@ -88,92 +93,20 @@ internal abstract class WhatsNewContent { } } - abstract fun getRequest(dataContext: DataContext?): HTMLEditorProvider.Request - abstract fun getActionWhiteList(): Set + abstract suspend fun show( + project: Project, + dataContext: DataContext?, + triggeredByUser: Boolean, + reactionChecker: FUSReactionChecker, + ) + abstract fun getVersion(): ContentVersion? abstract suspend fun isAvailable(): Boolean - - protected fun getHandler(dataContext: DataContext?): JsQueryHandler? { - dataContext ?: return null - - return object : JsQueryHandler { - override suspend fun query(id: Long, request: String): String { - val contains = getActionWhiteList().contains(request) - if (!contains) { - logger.trace { "EapWhatsNew action $request not allowed" } - WhatsNewCounterUsageCollector.actionNotAllowed(dataContext.project, request) - return "false" - } - - if (request.isNotEmpty()) { - service().getAction(request)?.let { - withContext(Dispatchers.EDT) { - it.actionPerformed( - AnActionEvent.createEvent( - it, - dataContext, - /*presentation =*/ null, - WhatsNewAction.PLACE, - ActionUiKind.NONE, - /*event =*/ null - ) - ) - logger.trace { "EapWhatsNew action $request performed" } - WhatsNewCounterUsageCollector.actionPerformed(dataContext.project, request) - } - return "true" - } ?: run { - logger.trace { "EapWhatsNew action $request not found" } - WhatsNewCounterUsageCollector.actionNotFound(dataContext.project, request) - } - } - return "false" - } - } - } } internal class WhatsNewUrlContent(val url: String) : WhatsNewContent() { companion object { val LOG = logger() - private val actionWhiteList = mutableSetOf("SearchEverywhere", "ChangeLaf", "ChangeIdeScale", - "SettingsSyncOpenSettingsAction", "BuildWholeSolutionAction", - "GitLab.Open.Settings.Page", - "AIAssistant.ToolWindow.ShowOrFocus", "ChangeMainToolbarColor", - "ShowEapDiagram", "multilaunch.RunMultipleProjects", - "EfCore.Shared.OpenQuickEfCoreActionsAction", - "OpenNewTerminalEAP", "CollectionsVisualizerEAP", "ShowDebugMonitoringToolEAP", - "LearnMoreStickyScrollEAP", "NewRiderProject", "BlazorHotReloadEAP") - } - - override fun getRequest(dataContext: DataContext?): HTMLEditorProvider.Request { - val parameters = HashMap() - parameters["var"] = "embed" - if (StartupUiUtil.isDarkTheme) { - parameters["theme"] = "dark" - } - parameters["lang"] = getCurrentLanguageTag() - val request = url(newFromEncoded(url).addParameters(parameters).toExternalForm()) - try { - WhatsNewAction::class.java.getResourceAsStream("whatsNewTimeoutText.html").use { stream -> - if (stream != null) { - request.withTimeoutHtml(String(stream.readAllBytes(), StandardCharsets.UTF_8).replace("__THEME__", - if (StartupUiUtil.isDarkTheme) "theme-dark" else "") - .replace("__TITLE__", IdeBundle.message("whats.new.timeout.title")) - .replace("__MESSAGE__", IdeBundle.message("whats.new.timeout.message")) - .replace("__ACTION__", IdeBundle.message("whats.new.timeout.action", url))) - } - } - } - catch (e: IOException) { - LOG.error(e) - } - request.withQueryHandler(getHandler(dataContext)) - return request - } - - override fun getActionWhiteList(): Set { - return actionWhiteList } override fun getVersion(): ContentVersion? = null @@ -191,7 +124,7 @@ internal class WhatsNewUrlContent(val url: String) : WhatsNewContent() { connection.instanceFollowRedirects = false connection.connect() - if (connection.responseCode != 200) { + if (connection.responseCode >= 400) { LOG.warn("WhatsNew page '$url' not available response code: ${connection.responseCode}") false } @@ -205,11 +138,15 @@ internal class WhatsNewUrlContent(val url: String) : WhatsNewContent() { } } } + + override suspend fun show(project: Project, dataContext: DataContext?, triggeredByUser: Boolean, reactionChecker: FUSReactionChecker) { + BrowserUtil.browse(IdeUrlTrackingParametersProvider.getInstance().augmentUrl(url)) + } } internal class WhatsNewVisionContent(page: WhatsNewInVisionContentProvider.Page) : WhatsNewContent() { companion object { - private const val THEME_KEY = "\$__VISION_PAGE_SETTINGS_THEME__\$" + private const val THEME_KEY = "\$__VISION_PAGE_SETTINGS_THEME__$" private const val DARK_THEME = "dark" private const val LIGHT_THEME = "light" @@ -233,16 +170,12 @@ internal class WhatsNewVisionContent(page: WhatsNewInVisionContentProvider.Page) contentHash = DigestUtil.sha1Hex(page.html) } - override fun getRequest(dataContext: DataContext?): HTMLEditorProvider.Request { + private fun getRequest(dataContext: DataContext?): HTMLEditorProvider.Request { val request = html(content) request.withQueryHandler(getHandler(dataContext)) return request } - override fun getActionWhiteList(): Set { - return myActionWhiteList - } - override fun getVersion(): ContentVersion { val buildNumber = ApplicationInfo.getInstance().getBuild() return ContentVersion( @@ -256,6 +189,74 @@ internal class WhatsNewVisionContent(page: WhatsNewInVisionContentProvider.Page) override suspend fun isAvailable(): Boolean { return true } + + private fun getHandler(dataContext: DataContext?): JsQueryHandler? { + dataContext ?: return null + + return object : JsQueryHandler { + override suspend fun query(id: Long, request: String): String { + val contains = myActionWhiteList.contains(request) + if (!contains) { + logger.trace { "EapWhatsNew action $request not allowed" } + WhatsNewCounterUsageCollector.actionNotAllowed(dataContext.project, request) + return "false" + } + + if (request.isNotEmpty()) { + service().getAction(request)?.let { + withContext(Dispatchers.EDT) { + it.actionPerformed( + AnActionEvent.createEvent( + it, + dataContext, + /*presentation =*/ null, + PLACE, + ActionUiKind.NONE, + /*event =*/ null + ) + ) + logger.trace { "EapWhatsNew action $request performed" } + WhatsNewCounterUsageCollector.actionPerformed(dataContext.project, request) + } + return "true" + } ?: run { + logger.trace { "EapWhatsNew action $request not found" } + WhatsNewCounterUsageCollector.actionNotFound(dataContext.project, request) + } + } + return "false" + } + } + } + + override suspend fun show(project: Project, dataContext: DataContext?, triggeredByUser: Boolean, reactionChecker: FUSReactionChecker) { + if (!JBCefApp.isSupported()) { + logger.error("EapWhatsNew: can't be shown. JBCefApp isn't supported") + } + + val title = IdeBundle.message("update.whats.new", ApplicationNamesInfo.getInstance().fullProductName) + withContext(Dispatchers.EDT) { + logger.info("Opening What's New in editor.") + val editor = writeIntentReadAction { openEditor(project, title, getRequest(dataContext)) } + editor?.let { + project.serviceAsync().addTopComponent(it, ReactionsPanel.createPanel(PLACE, reactionChecker)) + WhatsNewCounterUsageCollector.openedPerformed(project, triggeredByUser) + + WhatsNewContentVersionChecker.saveLastShownContent(this@WhatsNewVisionContent) + + val disposable = Disposer.newDisposable(project) + val busConnection = application.messageBus.connect(disposable) + busConnection.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, object : FileEditorManagerListener { + override fun fileClosed(source: FileEditorManager, file: VirtualFile) { + if (it.file == file) { + WhatsNewCounterUsageCollector.closedPerformed(project) + Disposer.dispose(disposable) + } + } + }) + } + } + } } fun getCurrentLanguageTag(): String { @@ -265,4 +266,7 @@ fun getCurrentLanguageTag(): String { } } +private val DataContext.project: Project? + get() = CommonDataKeys.PROJECT.getData(this) + private val logger = logger() diff --git a/platform/whatsNew/src/com/intellij/platform/whatsNew/WhatsNewContentVersionChecker.kt b/platform/whatsNew/src/com/intellij/platform/whatsNew/WhatsNewContentVersionChecker.kt index b527ed28c3ea..04f8b27fea8e 100644 --- a/platform/whatsNew/src/com/intellij/platform/whatsNew/WhatsNewContentVersionChecker.kt +++ b/platform/whatsNew/src/com/intellij/platform/whatsNew/WhatsNewContentVersionChecker.kt @@ -1,4 +1,4 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.platform.whatsNew import com.intellij.ide.util.PropertiesComponent @@ -12,6 +12,11 @@ internal class WhatsNewContentVersionChecker { fun isNeedToShowContent(whatsNewContent: WhatsNewContent): Boolean { + val newVersion = whatsNewContent.getVersion() ?: run { + LOG.info("What's New content provider returns null version. What's New will be ignored.") + return false + } + val savedVersionInfo = PropertiesComponent.getInstance().getValue(LAST_SHOWN_EAP_VERSION_PROP) ?: run { LOG.info("$LAST_SHOWN_EAP_VERSION_PROP is not defined. Will show What's New.") return true @@ -21,11 +26,6 @@ internal class WhatsNewContentVersionChecker { return true } - val newVersion = whatsNewContent.getVersion() ?: run { - LOG.warn("What's New content provider returns null version. What's New will be ignored.") - return false - } - val result = shouldShowWhatsNew(savedVersion, newVersion) LOG.info("Comparing versions $newVersion and $savedVersion: $result.") return result diff --git a/platform/whatsNew/testSrc/com/intellij/platform/whatsNew/OnStartCheckServiceTest.kt b/platform/whatsNew/testSrc/com/intellij/platform/whatsNew/OnStartCheckServiceTest.kt index be822250ab2a..fd8fedf8ee7a 100644 --- a/platform/whatsNew/testSrc/com/intellij/platform/whatsNew/OnStartCheckServiceTest.kt +++ b/platform/whatsNew/testSrc/com/intellij/platform/whatsNew/OnStartCheckServiceTest.kt @@ -1,8 +1,9 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.platform.whatsNew import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.project.Project +import com.intellij.platform.whatsNew.reaction.FUSReactionChecker import com.intellij.testFramework.junit5.TestApplication import com.intellij.testFramework.junit5.fixture.projectFixture import kotlinx.coroutines.runBlocking @@ -17,8 +18,9 @@ class OnStartCheckServiceTest { private val project = projectFixture() private val mockContent = object : WhatsNewContent() { - override fun getRequest(dataContext: DataContext?) = error("Mock object, do not call") - override fun getActionWhiteList() = error("Mock object, do not call") + override suspend fun show(project: Project, dataContext: DataContext?, triggeredByUser: Boolean, reactionChecker: FUSReactionChecker) { + error("Mock object, do not call") + } override fun getVersion() = error("Mock object, do not call") override suspend fun isAvailable() = error("Mock object, do not call") }