diff --git a/build.gradle.kts b/build.gradle.kts index 3eecc7e25..4bdfbdc89 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -40,6 +40,7 @@ subprojects { repositories { mavenCentral() maven("https://www.jetbrains.com/intellij-repository/releases") + maven("https://www.jetbrains.com/intellij-repository/snapshots") maven("https://cache-redirector.jetbrains.com/intellij-dependencies") } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index cffaec99e..41a630794 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -21,12 +21,33 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ + +import java.util.* + plugins { `kotlin-dsl` } repositories { mavenCentral() + maven("https://www.jetbrains.com/intellij-repository/releases") + maven("https://www.jetbrains.com/intellij-repository/snapshots") + maven("https://cache-redirector.jetbrains.com/intellij-dependencies") +} + + +val gradleProperties = Properties() +val gradlePropertiesFile = project.file("../gradle.properties") +if (gradlePropertiesFile.canRead()) { + gradleProperties.load(gradlePropertiesFile.inputStream()) +} + +val intellijPlatformVersion: String by gradleProperties + +dependencies { + implementation("com.jetbrains.intellij.platform:core:$intellijPlatformVersion") { + exclude(group = "org.jetbrains.kotlin") // cannot find these dependencies + } } kotlin { diff --git a/gradle.properties b/gradle.properties index ef742bbc3..04a991768 100644 --- a/gradle.properties +++ b/gradle.properties @@ -27,8 +27,8 @@ dnsjavaVersion=3.4.3 electronVersion=^16.0.6 electronPackagerVersion=^15.1.0 gradleMkdocsPluginVersion=2.2.0 -intellijPlatformVersion=213.6461.23 -intellijMarkdownPluginVersion=213.5744.223 +intellijPlatformVersion=213.6777.52 +intellijMarkdownPluginVersion=213.6777.52 intellijJcefVersion=89.0.12-g2b76680-chromium-89.0.4389.90-api-1.6 istanbulInstrumenterLoaderVersion=3.0.1 javassistVersion=3.28.0-GA diff --git a/projector-agent-common/build.gradle.kts b/projector-agent-common/build.gradle.kts index 76934e0f6..bd8de459b 100644 --- a/projector-agent-common/build.gradle.kts +++ b/projector-agent-common/build.gradle.kts @@ -37,9 +37,12 @@ publishToSpace("java") val javassistVersion: String by project val kotestVersion: String by project +val kotlinVersion: String by project dependencies { implementation(project(":projector-util-loading")) + + implementation(kotlin("reflect", kotlinVersion)) implementation("org.javassist:javassist:$javassistVersion") testImplementation("io.kotest:kotest-assertions-core:$kotestVersion") testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion") diff --git a/projector-agent-common/src/main/kotlin/org/jetbrains/projector/agent/common/Misc.kt b/projector-agent-common/src/main/kotlin/org/jetbrains/projector/agent/common/Misc.kt index 85ebe5a7b..6495994bc 100644 --- a/projector-agent-common/src/main/kotlin/org/jetbrains/projector/agent/common/Misc.kt +++ b/projector-agent-common/src/main/kotlin/org/jetbrains/projector/agent/common/Misc.kt @@ -27,17 +27,127 @@ package org.jetbrains.projector.agent.common import javassist.* +import org.jetbrains.projector.util.loading.ProjectorClassLoader import org.jetbrains.projector.util.loading.UseProjectorLoader +import java.lang.IllegalArgumentException +import java.lang.reflect.Constructor +import java.lang.reflect.Method +import java.lang.reflect.Modifier +import kotlin.reflect.KFunction +import kotlin.reflect.full.createType +import kotlin.reflect.jvm.javaConstructor +import kotlin.reflect.jvm.javaMethod internal fun getClassFromClassfileBuffer(pool: ClassPool, className: String, classfileBuffer: ByteArray): CtClass { pool.insertClassPath(ByteArrayClassPath(className, classfileBuffer)) return pool.get(className).apply(CtClass::defrost) } -private val currentClassPool: ClassPool by lazy { ClassPool().apply { appendClassPath(LoaderClassPath(object {}.javaClass.classLoader)) } } +public val projectorClassPool: ClassPool by lazy { ClassPool().apply { appendClassPath(LoaderClassPath(ProjectorClassLoader.instance)) } } private fun CtClass.getDeclaredMethodImpl(name: String, classPool: ClassPool, params: Array>): CtMethod = getDeclaredMethod(name, params.map { classPool[it.name] }.toTypedArray()) public fun CtClass.getDeclaredMethod(name: String, vararg params: Class<*>): CtMethod = - getDeclaredMethodImpl(name, currentClassPool, params) + getDeclaredMethodImpl(name, projectorClassPool, params) + +private fun CtClass.getDeclaredConstructorImpl(classPool: ClassPool, params: Array>): CtConstructor = + getDeclaredConstructor(params.map { classPool[it.name] }.toTypedArray()) + +public fun CtClass.getDeclaredConstructor(vararg params: Class<*>): CtConstructor = + getDeclaredConstructorImpl(projectorClassPool, params) + +public inline operator fun ClassPool.invoke(): CtClass = this[T::class.java.name] + +public fun Method.toGetDeclaredMethodFormat(): String { + + //val parameterClasses = parameterTypes.joinToString(", ") { "${it.kotlin.javaObjectType.name}.class" } + val parameterClasses = parameterTypes.joinToString(", ") { "${it.name}.class" } + val parametersString = "new Class[] { $parameterClasses }" + + return "\"$name\", $parametersString" +} + +public fun Constructor<*>.toGetDeclaredMethodFormat(): String { + val parameterClasses = parameterTypes.joinToString(", ") { "${it.name}.class" } + return "new Class[] { $parameterClasses }" +} + +public fun loadClassWithProjectorLoader(clazz: Class<*>): String = loadClassWithProjectorLoader(clazz.name, true) + +private fun loadClassWithProjectorLoader(className: String, trim: Boolean): String = """ + $commonClassLoadCode + .loadClass("$className") + """.let { if (!trim) it.trimIndent() else it } + +private val unitType by lazy { Unit::class.createType() } + +public fun > T.getJavaCallString( + vararg params: String, + finishedExpression: Boolean = this.returnType == unitType, + autoCast: Boolean = true, +): String { + + val mainPart = when { + javaMethod != null -> getJavaCallString(javaMethod!!, autoCast, *params) + javaConstructor != null -> getJavaCallString(javaConstructor!!, autoCast, *params) + else -> throw IllegalArgumentException("Cannot convert Kotlin function $this to Java method") + } + + return if (finishedExpression) + """ + { + $mainPart; + } + """.trimIndent() + else mainPart +} + +private fun getJavaCallString(asJavaMethod: Method, cast: Boolean = true, vararg params: String): String { + + val isStatic = Modifier.isStatic(asJavaMethod.modifiers) + val instance = if (isStatic) "null" else params.first() + val otherParams = if (isStatic) params.toList() else params.drop(1) + + require(otherParams.size == asJavaMethod.parameterCount) { + "Cannot create Java method call string: expected ${asJavaMethod.parameterCount} parameters, got ${otherParams.size}" + } + + val castString = if (!cast || asJavaMethod.returnType == Void.TYPE) "" else "(${asJavaMethod.returnType.objectType.name})" + + return """ + ($castString ${loadClassWithProjectorLoader(asJavaMethod.declaringClass.name, false)} + .getDeclaredMethod(${asJavaMethod.toGetDeclaredMethodFormat()}) + .invoke($instance, new Object[] { ${otherParams.joinToString(", ")} })) + """.trimIndent() +} + +private fun getJavaCallString(asJavaCtor: Constructor<*>, cast: Boolean = true, vararg params: String): String { + require(params.size == asJavaCtor.parameterCount) { + "Cannot create Java method call string: expected ${asJavaCtor.parameterCount} parameters, got ${params.size}" + } + + val castString = if (!cast) "" else "(${asJavaCtor.declaringClass.name})" + + return """ + ($castString ${loadClassWithProjectorLoader(asJavaCtor.declaringClass)} + .getDeclaredConstructor(${asJavaCtor.toGetDeclaredMethodFormat()}) + .newInstance(new Object[] { ${params.joinToString(", ")} })) + """.trimIndent() +} + +private val Class<*>.objectType get() = kotlin.javaObjectType + +public const val assign: String = "${'$'}_ = " + +private val PROJECTOR_LOADER_CLASS_NAME: String = ProjectorClassLoader::class.java.name + +private val PROJECTOR_LOADER_INSTANCE_GETTER_NAME: String = ProjectorClassLoader.Companion::instance.getter.javaMethod!!.name + +private val commonClassLoadCode = """ + ((ClassLoader) ClassLoader + .getSystemClassLoader() + .loadClass("$PROJECTOR_LOADER_CLASS_NAME") + .getDeclaredMethod("$PROJECTOR_LOADER_INSTANCE_GETTER_NAME", new Class[0]) + .invoke(null, new Object[0])) + """.trimIndent() diff --git a/projector-agent-ij-injector/build.gradle.kts b/projector-agent-ij-injector/build.gradle.kts index a202eeb92..38c253025 100644 --- a/projector-agent-ij-injector/build.gradle.kts +++ b/projector-agent-ij-injector/build.gradle.kts @@ -43,6 +43,7 @@ val kotestVersion: String by project dependencies { implementation(project(":projector-agent-common")) + implementation(project(":projector-common")) implementation(project(":projector-ij-common")) implementation(project(":projector-util-loading")) implementation(project(":projector-util-logging")) diff --git a/projector-agent-ij-injector/src/main/kotlin/org/jetbrains/projector/agent/ijInjector/IjInjector.kt b/projector-agent-ij-injector/src/main/kotlin/org/jetbrains/projector/agent/ijInjector/IjInjector.kt index 80a2a0d2a..77cdcec7f 100644 --- a/projector-agent-ij-injector/src/main/kotlin/org/jetbrains/projector/agent/ijInjector/IjInjector.kt +++ b/projector-agent-ij-injector/src/main/kotlin/org/jetbrains/projector/agent/ijInjector/IjInjector.kt @@ -24,6 +24,7 @@ package org.jetbrains.projector.agent.ijInjector import org.jetbrains.projector.agent.init.IjArgs +import org.jetbrains.projector.ij.jcef.isCefAvailable import org.jetbrains.projector.util.loading.UseProjectorLoader import java.lang.instrument.Instrumentation @@ -34,7 +35,9 @@ internal object IjInjector { class AgentParameters( val isAgent: Boolean, val markdownPanelClassName: String, - ) + ) { + val jcefTransformerInUse by lazy { isCefAvailable() } + } private fun parametersFromArgs(args: Map): AgentParameters { @@ -52,6 +55,7 @@ internal object IjInjector { val transformers = listOf( IjLigaturesDisablerTransformer, IjMdTransformer, + IjJcefTransformer, IjBrowserUtilTransformer, IjUiUtilsTransformer, IjFastNodeCellRendererTransformer, diff --git a/projector-agent-ij-injector/src/main/kotlin/org/jetbrains/projector/agent/ijInjector/IjJcefTransformer.kt b/projector-agent-ij-injector/src/main/kotlin/org/jetbrains/projector/agent/ijInjector/IjJcefTransformer.kt new file mode 100644 index 000000000..da1a4aee5 --- /dev/null +++ b/projector-agent-ij-injector/src/main/kotlin/org/jetbrains/projector/agent/ijInjector/IjJcefTransformer.kt @@ -0,0 +1,439 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 JetBrains s.r.o. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jetbrains.projector.agent.ijInjector + +import com.intellij.ui.jcef.* +import javassist.* +import javassist.expr.* +import org.cef.CefApp +import org.cef.CefClient +import org.cef.CefSettings +import org.cef.browser.CefBrowserFactory +import org.cef.browser.CefRendering +import org.cef.browser.CefRequestContext +import org.cef.handler.CefClientHandler +import org.jetbrains.projector.agent.common.* +import org.jetbrains.projector.common.intellij.buildAtLeast +import org.jetbrains.projector.ij.jcef.CefHandlers +import org.jetbrains.projector.ij.jcef.ProjectorCefBrowser +import org.jetbrains.projector.util.loading.state.IdeState + +internal object IjJcefTransformer : IdeTransformerSetup() { + + private var isAgent = false + + override fun getTransformations( + parameters: IjInjector.AgentParameters, + classLoader: ClassLoader, + ): Map, (CtClass) -> ByteArray?> { + + isAgent = parameters.isAgent + + val transformations = mutableMapOf, (CtClass) -> ByteArray?>( + CefApp::class.java to ::transformCefApp, + CefBrowserFactory::class.java to ::transformCefBrowserFactory, + CefClientHandler::class.java to ::transformCefClientHandler, + Class.forName("org.cef.browser.CefMessageRouter_N") to ::transformNativeCefMessageRouter, + JBCefClient::class.java to ::transformJBCefClient, + JBCefJSQuery::class.java to ::transformJBCefJSQuery, + Class.forName("com.intellij.ui.jcef.JBCefFileSchemeHandlerFactory") to ::transformJBSchemeHandlerFactory, + ) + + if (buildAtLeast("211")) { + transformations += JBCefBrowserBase::class.java to ::transformJBCefBrowserBase + } + + return transformations + } + + override fun isTransformerAvailable(parameters: IjInjector.AgentParameters): Boolean { + return parameters.jcefTransformerInUse + } + + override val loadingState: IdeState + get() = IdeState.CONFIGURATION_STORE_INITIALIZED + + private fun transformJBSchemeHandlerFactory(clazz: CtClass): ByteArray { + + if (isAgent) { + + val nativeBrowserCode = ProjectorCefBrowser::originalBrowser.getter.getJavaCallString("$1") + + @Suppress("UnusedAssignment") + clazz + .getDeclaredMethod("registerLoadHTMLRequest") + .insertBefore( + // language=java prefix="class JBCefFileSchemeHandlerFactory { public static String registerLoadHTMLRequest(@NotNull org.cef.browser.CefBrowser $1, @NotNull String $2, @NotNull String $3)" suffix="}" + """ + { + // in agent mode native browser is saved to map, but ProjectorCefBrowser is passed to getter + if ($1.getClass().getName().equals("${ProjectorCefBrowser::class.java.name}")) { + $1 = $nativeBrowserCode; + } + } + """.trimIndent() + ) + } + + return clazz.toBytecode() + } + + private fun transformJBCefJSQuery(clazz: CtClass): ByteArray { + + if (buildAtLeast("211")) { // version where warning appeared + + /** + * Get rid of warning with stacktrace due to [JBCefBrowserBase.isCefBrowserCreated] made always return true + */ + clazz + .getDeclaredMethods("create") // there are two of them in 221 + .forEach { + it.setBody( + // language=java prefix="class JBCefJSQuery { public static com.intellij.ui.jcef.JBCefJSQuery create(@NotNull com.intellij.ui.jcef.JBCefBrowserBase $1)" suffix="}" + """ + { + return new com.intellij.ui.jcef.JBCefJSQuery($1, new com.intellij.ui.jcef.JBCefJSQuery.JSQueryFunc($1.getJBCefClient())); + } + """.trimIndent() + ) + } + + } + + return clazz.toBytecode() + } + + private fun CtBehavior.setBodyIfHeadless(body: String) { + if (isAgent) return + setBody(body) + } + + private fun CtBehavior.insertAfterIfHeadless(body: String) { + if (isAgent) return + insertAfter(body) + } + + private fun CtBehavior.setBodyOrInsertBefore(body: String) { + when (isAgent) { + true -> insertBefore(body) + false -> setBody(body) + } + } + + private fun CtBehavior.setBodyOrInsertAfter(body: String) { + when (isAgent) { + true -> insertAfter(body) + false -> setBody(body) + } + } + + private fun CtBehavior.removeNativeCallsIfHeadless() { + if (isAgent) return + instrument(object : ExprEditor() { + override fun edit(m: MethodCall) { + if (Modifier.isNative(m.method.modifiers)) { + m.replace("") + } + } + }) + } + + private fun transformCefClientHandler(clazz: CtClass): ByteArray { + + clazz + .getDeclaredConstructor() + .removeNativeCallsIfHeadless() + + clazz + .getDeclaredMethod("dispose") + .removeNativeCallsIfHeadless() + + clazz + .getDeclaredMethod("addMessageRouter") + // language=java prefix="class CefClientHandler { protected synchronized void addMessageRouter(org.cef.browser.CefMessageRouter $1) {" suffix="}}" + .setBodyOrInsertBefore(CefHandlers::onMessageRouterAdded.getJavaCallString("$0", "$1")) + + clazz + .getDeclaredMethod("removeMessageRouter") + // language=java prefix="class CefClientHandler { protected synchronized void removeMessageRouter(org.cef.browser.CefMessageRouter $1) {" suffix="}}" + .setBodyOrInsertBefore(CefHandlers::onMessageRouterRemoved.getJavaCallString("$0", "$1")) + + listOf( + "removeContextMenuHandler", + "removeDialogHandler", + "removeDisplayHandler", + "removeDownloadHandler", + "removeDragHandler", + "removeFocusHandler", + "removeJSDialogHandler", + "removeKeyboardHandler", + "removeLifeSpanHandler", + "removeLoadHandler", + "removeRenderHandler", + "removeRequestHandler", + "removeWindowHandler", + ).forEach { methodName -> + + clazz + .getDeclaredMethod(methodName) + .removeNativeCallsIfHeadless() + } + + return clazz.toBytecode() + } + + private fun transformCefApp(clazz: CtClass): ByteArray { + + @Suppress("rawtypes") + clazz + .getDeclaredConstructor(Array::class.java, CefSettings::class.java) + .setBodyIfHeadless( + // language=java prefix="class CefApp { private CefApp(String[] $1, org.cef.CefSettings $2)" suffix="}" + """ + { + super($1); + if ($2 != null) settings_ = $2.clone(); + clients_ = new java.util.HashSet(); + } + """.trimIndent() + ) + + clazz + .getDeclaredMethod("startup") + // language=java prefix="class CefApp { public static boolean startup(String[] $1)" suffix="}" + .setBodyIfHeadless( + """ + { + return true; + } + """.trimIndent() + ) + + clazz + .getDeclaredMethod("initialize") + .setBodyIfHeadless( + // language=java prefix="class CefMessageRouter_N { public boolean addHandler(CefMessageRouterHandler $1, boolean $2)" suffix="}" + """ + { + setState(org.cef.CefApp.CefAppState.INITIALIZED); + } + """.trimIndent() + ) + + return clazz.toBytecode() + } + + private fun transformNativeCefMessageRouter(clazz: CtClass): ByteArray { + + // javassist can't automatically box primitives.. + val addHandlerCode = CefHandlers::addMessageRouterHandler.getJavaCallString("this", "$1", "Boolean.valueOf($2)") + val removeHandlerCode = CefHandlers::removeMessageRouterHandler.getJavaCallString("this", "$1") + val clearRouterHandlersCode = CefHandlers::clearMessageRouterHandlers.getJavaCallString("this") + + @Suppress("rawtypes", "unchecked") + clazz + .getDeclaredMethod("addHandler") + .apply { + if (isAgent) { + insertBefore( + """ + { + $addHandlerCode; + } + """.trimIndent() + ) + } + else { + setBody( + // language=java prefix="class CefMessageRouter_N { public boolean addHandler(CefMessageRouterHandler $1, boolean $2)" suffix="}" + """ + { + return $addHandlerCode.booleanValue(); // javassist can't automatically unbox primitive wrapper.. + } + """.trimIndent() + ) + } + } + + clazz + .getDeclaredMethod("removeHandler") + .apply { + if (isAgent) { + insertBefore( + // language=java prefix="class CefMessageRouter_N { public boolean removeHandler(CefMessageRouterHandler $1)" suffix="}" + """ + { + $removeHandlerCode; + } + """.trimIndent() + ) + } + else { + setBody( + // language=java prefix="class CefMessageRouter_N { public boolean removeHandler(CefMessageRouterHandler $1)" suffix="}" + """ + { + return $removeHandlerCode.booleanValue(); // javassist can't automatically unbox primitive wrapper.. + } + """.trimIndent() + ) + } + } + + clazz + .getDeclaredMethod("createNative") + .setBodyIfHeadless( + // language=java prefix="class CefMessageRouter_N { public static org.cef.browser.CefMessageRouter createNative(org.cef.browser.CefMessageRouterConfig $1)" suffix="}" + """ + { + org.cef.browser.CefMessageRouter instance = new org.cef.browser.CefMessageRouter_N(); + instance.setMessageRouterConfig($1); + return instance; + } + """.trimIndent() + ) + + clazz + .getDeclaredMethod("dispose") + .setBodyOrInsertBefore(clearRouterHandlersCode) + + return clazz.toBytecode() + } + + private fun transformJBCefClient(clazz: CtClass): ByteArray { + + clazz + .getDeclaredMethod("addLifeSpanHandler") + .insertAfterIfHeadless(CefHandlers::onLifeSpanHandlerAdded.getJavaCallString("myCefClient", "$1")) + + clazz + .getDeclaredMethod("removeLifeSpanHandler") + .insertAfterIfHeadless(CefHandlers::onLifeSpanHandlerRemoved.getJavaCallString("myCefClient")) + + return clazz.toBytecode() + } + + private fun transformJBCefBrowserBase(clazz: CtClass): ByteArray { + + if (buildAtLeast("212")) { + + clazz + .getDeclaredConstructor(JBCefBrowserBuilder::class.java) + .instrument(JBCefBrowserBaseProjectorInserter()) + + } + + if (buildAtLeast("213")) { + + fun sendSetOpenLinksInExternalBrowserString(openLinksInExternalBrowser: Boolean): String { + val booleanParam = "Boolean.valueOf($openLinksInExternalBrowser)" + return ProjectorCefBrowser::setOpenLinksInExternalBrowser.getJavaCallString("getCefBrowser()", booleanParam) + } + + clazz + .getDeclaredMethod("enableExternalBrowserLinks") + .setBodyOrInsertBefore(sendSetOpenLinksInExternalBrowserString(true)) + + clazz + .getDeclaredMethod("disableExternalBrowserLinks") + .setBodyOrInsertBefore(sendSetOpenLinksInExternalBrowserString(false)) + } + + // Get rid of checking by referencing to underlying CEF browser pointer + clazz + .getDeclaredMethods("isCefBrowserCreated") + .forEach { + it.setBodyIfHeadless( + // language=java prefix="class JBCefBrowserBase { final boolean isCefBrowserCreated()" suffix="}" + """ + { + return true; + } + """.trimIndent() + ) + } + + return clazz.toBytecode() + } + + private fun getProjectorCefBrowserInstantiationCode(vararg params: String): String { + return "(org.cef.browser.CefBrowser) ${::ProjectorCefBrowser.getJavaCallString(*params, autoCast = false)}" + } + + private fun transformCefBrowserFactory(clazz: CtClass): ByteArray { + + val originalBrowserParameter = if (isAgent) "${'$'}_" else "null" + val newBrowserInstanceCode = getProjectorCefBrowserInstantiationCode("$1", "$2", originalBrowserParameter) + + val thirdParameterType = if (buildAtLeast("211")) CefRendering::class.java else Boolean::class.java + + clazz + .getDeclaredMethod("create", + CefClient::class.java, String::class.java, + thirdParameterType, Boolean::class.java, CefRequestContext::class.java) + .setBodyOrInsertAfter( + """ + { + return $newBrowserInstanceCode; + } + """.trimIndent() + ) + + return clazz.toBytecode() + } + + /** + * Replace instantiation of CEF browser in JBCefBrowserBase with ProjectorCefBrowser + */ + private class JBCefBrowserBaseProjectorInserter : ExprEditor() { + + private val methodDeclaringClass = projectorClassPool[METHOD_DECLARING_CLASS] + + private val originalBrowserParameter = if (isAgent) "${'$'}proceed($$)" else "null" + private val newBrowserInstanceCode = getProjectorCefBrowserInstantiationCode("$2", "$3", originalBrowserParameter) + + override fun edit(m: MethodCall) { + val ctMethod = m.method + if (ctMethod.declaringClass.subclassOf(methodDeclaringClass) && ctMethod.name == OSR_CREATOR_METHOD_NAME) { + + @Suppress("rawtypes", "unchecked") + //language=java prefix="class JBCefBrowserBase { private @NotNull CefBrowserOsrWithHandler createOsrBrowser(...)" suffix="}" + val code = """ + { + $assign $newBrowserInstanceCode; + } + """.trimIndent() + + // replace method call instead of called method body because method returns CefBrowserOsrWithHandler, but we only have CefBrowser + m.replace(code) + } + } + + companion object { + private const val OSR_CREATOR_METHOD_NAME = "createOsrBrowser" + private val METHOD_DECLARING_CLASS = JBCefBrowserBase::class.java.name + } + } + +} diff --git a/projector-agent-ij-injector/src/main/kotlin/org/jetbrains/projector/agent/ijInjector/IjMdTransformer.kt b/projector-agent-ij-injector/src/main/kotlin/org/jetbrains/projector/agent/ijInjector/IjMdTransformer.kt index d5c045257..59ec94ec1 100644 --- a/projector-agent-ij-injector/src/main/kotlin/org/jetbrains/projector/agent/ijInjector/IjMdTransformer.kt +++ b/projector-agent-ij-injector/src/main/kotlin/org/jetbrains/projector/agent/ijInjector/IjMdTransformer.kt @@ -23,13 +23,20 @@ */ package org.jetbrains.projector.agent.ijInjector -import com.intellij.ide.plugins.PluginManagerCore -import com.intellij.openapi.extensions.PluginId +import com.intellij.openapi.project.Project import com.intellij.openapi.util.BuildNumber +import com.intellij.openapi.vfs.VirtualFile import com.intellij.ui.jcef.JBCefApp import javassist.CtClass +import javassist.expr.ExprEditor +import javassist.expr.NewExpr +import org.intellij.plugins.markdown.ui.preview.jcef.MarkdownJCEFHtmlPanel +import org.jetbrains.projector.agent.common.assign +import org.jetbrains.projector.agent.common.getDeclaredConstructor import org.jetbrains.projector.agent.common.transformation.TransformationResult import org.jetbrains.projector.agent.common.transformation.classForNameOrNull +import org.jetbrains.projector.common.intellij.buildAtLeast +import org.jetbrains.projector.common.intellij.buildInRange import org.jetbrains.projector.ij.md.markdownPlugin import org.jetbrains.projector.util.loading.ProjectorClassLoader import org.jetbrains.projector.util.loading.state.IdeState @@ -45,6 +52,9 @@ internal object IjMdTransformer : IdeTransformerSetup, (CtClass) -> ByteArray?>() - transformations += listOf( - javaFxClass to MdPreviewType.JAVAFX, - jcefClass to MdPreviewType.JCEF, - ).mapNotNull { (className, previewType) -> - val clazz = classForNameOrNull(className, classLoader) ?: run { - transformationResultConsumer(TransformationResult.Skip(this, className, "Class not found")) - return@mapNotNull null + if (!parameters.jcefTransformerInUse) { + transformations += listOf( + javaFxClass to MdPreviewType.JAVAFX, + jcefClass to MdPreviewType.JCEF, + ).mapNotNull { (className, previewType) -> + val clazz = classForNameOrNull(className, classLoader) ?: run { + transformationResultConsumer(TransformationResult.Skip(this, className, "Class not found")) + return@mapNotNull null + } + clazz to previewType + }.associate { (clazz, previewType) -> + val foo = { ctClass: CtClass -> + transformMdHtmlPanelProvider(previewType, ctClass, parameters.markdownPanelClassName, parameters.isAgent) + } + clazz to foo } - clazz to previewType - }.associate { (clazz, previewType) -> - clazz to { ctClass -> transformMdHtmlPanelProvider(previewType, ctClass, parameters.markdownPanelClassName, parameters.isAgent) } } - if (!parameters.isAgent && isPreviewCheckIsBrokenInHeadless()) { + if (isPreviewCheckBroken()) { val previewFileEditor = Class.forName(previewFileEditorClass, false, classLoader) transformations[previewFileEditor] = ::fixMarkdownPreviewNPE } + if (areResourcesProtected()) { + transformations += MarkdownJCEFHtmlPanel::class.java to ::transformMarkdownJCEFHtmlPanel + } + return transformations } @@ -94,14 +113,27 @@ internal object IjMdTransformer : IdeTransformerSetup= buildNumberMin && markdownBuildNumber < buildNumberFixed + private fun transformMarkdownJCEFHtmlPanel(clazz: CtClass): ByteArray { + + clazz + .getDeclaredConstructor(Project::class.java, VirtualFile::class.java) + .instrument(object : ExprEditor() { + override fun edit(e: NewExpr) { + if (e.className == pipeImplClass) { + // Disable whitelisting of allowed resources (because we inline them) + e.replace("$assign new $pipeImplClass(this, null);") + } + } + }) + + + return clazz.toBytecode() } + private fun areResourcesProtected() = BuildNumber.fromString(markdownPlugin!!.version)!!.buildAtLeast("221") + + private fun isPreviewCheckBroken() = BuildNumber.fromString(markdownPlugin!!.version)!!.buildInRange("201", "212") + private fun fixMarkdownPreviewNPE(previewFileEditorClazz: CtClass): ByteArray { @Suppress("deprecation") // copied from Markdown plugin 212 @@ -200,7 +232,8 @@ internal object IjMdTransformer : IdeTransformerSetup() - private val logger = Logger() + private val charPool : List = ('a'..'z') + ('A'..'Z') + ('0'..'9') - private val charPool : List = ('a'..'z') + ('A'..'Z') + ('0'..'9') - - private fun generateKey(): String = - (1 .. 20) - .map { Random.nextInt(0, charPool.size) } - .map(charPool::get) - .joinToString("") - } + private fun generateKey(): String = + (1 .. 20) + .map { Random.nextInt(0, charPool.size) } + .map(charPool::get) + .joinToString("") } diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/Main.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/Main.kt index d615820dc..d32578c3d 100644 --- a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/Main.kt +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/Main.kt @@ -25,7 +25,5 @@ package org.jetbrains.projector.client.web @JsExport fun onLoad() { - Application().apply { - start() - } + Application.start() } diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/ServerEventsProcessor.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/ServerEventsProcessor.kt index 7c0155e5b..151eaeec7 100644 --- a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/ServerEventsProcessor.kt +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/ServerEventsProcessor.kt @@ -24,6 +24,7 @@ package org.jetbrains.projector.client.web import org.jetbrains.projector.client.common.RenderingQueue +import org.jetbrains.projector.client.web.component.EmbeddedBrowserManager import org.jetbrains.projector.client.web.component.MarkdownPanelManager import org.jetbrains.projector.client.web.input.InputController import org.jetbrains.projector.client.web.misc.* @@ -47,7 +48,11 @@ class ServerEventsProcessor( private val clipboardHandler = ClipboardHandler { stateMachine.fire(ClientAction.AddEvent(it)) } fun process( - commands: ToClientMessageType, pingStatistics: PingStatistics, typing: Typing, markdownPanelManager: MarkdownPanelManager, + commands: ToClientMessageType, + pingStatistics: PingStatistics, + typing: Typing, + markdownPanelManager: MarkdownPanelManager, + embeddedBrowserManager: EmbeddedBrowserManager, inputController: InputController, ) { val drawCommandsEvents = mutableListOf() @@ -57,6 +62,7 @@ class ServerEventsProcessor( is ServerWindowSetChangedEvent -> { windowDataEventsProcessor.process(command) markdownPanelManager.updatePlacements() + embeddedBrowserManager.updatePlacements() } is ServerDrawCommandsEvent -> drawCommandsEvents.add(command) @@ -93,6 +99,17 @@ class ServerEventsProcessor( // todo: should WindowManager.lookAndFeelChanged() be called here? OnScreenMessenger.lookAndFeelChanged() } + + is ServerBrowserEvent -> when (command) { + is ServerBrowserEvent.ExecuteJsEvent -> embeddedBrowserManager.executeJs(command.browserId, command.code) + is ServerBrowserEvent.SetHtmlEvent -> embeddedBrowserManager.setHtml(command.browserId, command.html) + is ServerBrowserEvent.LoadUrlEvent -> embeddedBrowserManager.loadUrl(command.browserId, command.url, command.show) + is ServerBrowserEvent.ShowEvent -> embeddedBrowserManager.show(command.browserId, command.show, command.windowId) + is ServerBrowserEvent.MoveEvent -> embeddedBrowserManager.move(command.browserId, command.position) + is ServerBrowserEvent.ResizeEvent -> embeddedBrowserManager.resize(command.browserId, command.size) + is ServerBrowserEvent.SetOpenLinksInExternalBrowserEvent -> + embeddedBrowserManager.setOpenLinksInExternalBrowser(command.browserId, command.openLinksInExternalBrowser) + } } } diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/component/ClientComponent.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/component/ClientComponent.kt new file mode 100644 index 000000000..06319a0c3 --- /dev/null +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/component/ClientComponent.kt @@ -0,0 +1,97 @@ +/* + * MIT License + * + * Copyright (c) 2019-2022 JetBrains s.r.o. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jetbrains.projector.client.web.component + +import kotlinx.browser.document +import org.w3c.dom.HTMLIFrameElement + +abstract class ClientComponent( + protected val id: Int, +) { + + val iFrame: HTMLIFrameElement = createIFrame(id) + + var windowId: Int? = null + + fun dispose() { + iFrame.remove() + } + + private fun createIFrame(browserId: Int) = (document.createElement("iframe") as HTMLIFrameElement).apply { + id = getIFrameId(browserId) + style.apply { + position = "fixed" + backgroundColor = "#FFF" + overflowX = "scroll" + overflowY = "scroll" + display = "none" + } + + frameBorder = "0" + + document.body!!.appendChild(this) + + // cancel auto-started load of about:blank in Firefox + // https://stackoverflow.com/questions/7828502/cannot-set-document-body-innerhtml-of-iframe-in-firefox + contentDocument!!.apply { + open() + close() + } + + contentDocument!!.oncontextmenu = { false } + } + + protected fun setLinkProcessor(linkProcessor: (String) -> Unit) { + iFrame.contentDocument?.onclick = { e -> + var target = e.target.asDynamic() + while (target != null && target.tagName != "A") { + target = target.parentNode + } + + if (target == null) { + true + } + else if (target.tagName == "A" && target.hasAttribute("href").unsafeCast()) { + e.stopPropagation() + + val href = target.getAttribute("href").unsafeCast() + if (href[0] == '#') { + val elementId = href.substring(1) + iFrame.contentDocument?.getElementById(elementId)?.scrollIntoView() + } + else { + linkProcessor(href) + } + + false + } + else { + null + } + } + } + + private fun getIFrameId(browserId: Int) = "${this::class.simpleName}$browserId" + +} diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/component/ClientComponentManager.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/component/ClientComponentManager.kt new file mode 100644 index 000000000..83f5edf01 --- /dev/null +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/component/ClientComponentManager.kt @@ -0,0 +1,103 @@ +/* + * MIT License + * + * Copyright (c) 2019-2022 JetBrains s.r.o. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jetbrains.projector.client.web.component + +import org.jetbrains.projector.common.misc.Do +import org.jetbrains.projector.common.protocol.data.CommonIntSize +import org.jetbrains.projector.common.protocol.data.Point + +abstract class ClientComponentManager( + protected val zIndexByWindowIdGetter: (Int) -> Int?, +) { + + private val idToComponent = mutableMapOf() + + abstract fun createComponent(componentId: Int): Component + + protected fun getOrCreate(componentId: Int): Component { + return idToComponent.getOrPut(componentId) { createComponent(componentId) } + } + + fun placeToWindow(componentId: Int, windowId: Int) { + val panel = getOrCreate(componentId) + + panel.windowId = windowId + + val zIndex = zIndexByWindowIdGetter(windowId) ?: return + + panel.iFrame.style.zIndex = (zIndex + 1).toString() + } + + fun updatePlacements() { + idToComponent.forEach { (componentId, panel) -> + panel.windowId?.let { placeToWindow(componentId, it) } + } + } + + fun show(componentId: Int, show: Boolean, windowId: Int? = null) { + val component = getOrCreate(componentId) + + Do exhaustive when (show) { + true -> { + windowId?.also { placeToWindow(componentId, it) } + component.iFrame.style.display = "block" + } + + false -> component.iFrame.style.display = "none" + } + } + + fun move(componentId: Int, point: Point) { + val component = getOrCreate(componentId) + + component.iFrame.style.apply { + left = "${point.x}px" + top = "${point.y}px" + } + } + + fun resize(componentId: Int, size: CommonIntSize) { + val component = getOrCreate(componentId) + + component.iFrame.style.apply { + width = "${size.width}px" + height = "${size.height}px" + } + } + + fun dispose(componentId: Int) { + val component = getOrCreate(componentId) + + component.dispose() + + idToComponent.remove(componentId) + } + + fun disposeAll() { + idToComponent.values.forEach { it.dispose() } + + idToComponent.clear() + } + +} diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/component/EmbeddedBrowserManager.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/component/EmbeddedBrowserManager.kt new file mode 100644 index 000000000..cf8336688 --- /dev/null +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/component/EmbeddedBrowserManager.kt @@ -0,0 +1,150 @@ +/* + * MIT License + * + * Copyright (c) 2019-2022 JetBrains s.r.o. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jetbrains.projector.client.web.component + +import kotlinx.browser.document +import org.jetbrains.projector.client.web.UriHandler +import org.w3c.dom.HTMLScriptElement +import org.w3c.dom.events.MouseEvent + +class EmbeddedBrowserManager( + zIndexByWindowIdGetter: (Int) -> Int?, + private val openInExternalBrowser: (String) -> Unit, +) : ClientComponentManager(zIndexByWindowIdGetter) { + + class EmbeddedBrowserPanel(id: Int, private val openInExternalBrowser: (String) -> Unit) : ClientComponent(id) { + + var wasLoaded = false + + private var openLinksInExternalBrowser: Boolean = false + + private var previousClickHandler: ((MouseEvent) -> dynamic)? = null + + private val jsQueue = ArrayDeque() + + init { + iFrame.apply { + onload = { + setOpenLinksInExternalBrowser(openLinksInExternalBrowser, true) + + wasLoaded = true + while (jsQueue.isNotEmpty()) { + val code = jsQueue.removeFirst() + executeJsImpl(code) + } + } + } + } + + fun setHtml(html: String) { + iFrame.srcdoc = html + } + + fun executeJs(code: String) { + if (wasLoaded) { + executeJsImpl(code) + } + else { + jsQueue.addLast(code) + } + } + + private fun executeJsImpl(code: String) { + val elem = document.createElement("script") as HTMLScriptElement + + elem.type = "text/javascript" + elem.text = code + + iFrame.contentDocument!!.head!!.append(elem) + } + + private fun setOpenLinksInExternalBrowser(openLinksInExternalBrowser: Boolean, forceIfTrue: Boolean) { + val changed = openLinksInExternalBrowser != this.openLinksInExternalBrowser + if (!(changed || forceIfTrue)) { + return + } + this.openLinksInExternalBrowser = openLinksInExternalBrowser + if (!openLinksInExternalBrowser) { + if (changed) { + iFrame.contentDocument?.onclick = previousClickHandler + } + return + } + + if (changed) { + previousClickHandler = iFrame.contentDocument?.onclick + } + + setLinkProcessor(openInExternalBrowser) + } + + fun setOpenLinksInExternalBrowser(openLinksInExternalBrowser: Boolean) = setOpenLinksInExternalBrowser(openLinksInExternalBrowser, + false) + } + + fun setOpenLinksInExternalBrowser(browserId: Int, openLinksInExternalBrowser: Boolean) { + val panel = getOrCreate(browserId) + panel.setOpenLinksInExternalBrowser(openLinksInExternalBrowser) + } + + fun setHtml(browserId: Int, html: String) { + val panel = getOrCreate(browserId) + + panel.wasLoaded = false + panel.setHtml(html) + } + + fun loadUrl(browserId: Int, url: String, show: Boolean) { + val panel = getOrCreate(browserId) + + panel.setOpenLinksInExternalBrowser(true) + + // TODO add styles corresponding to IDE ones + panel.setHtml( + """ + + + + +

Following non-localhost link was opened in new window:

+ $url + + + """.trimIndent() + ) + + if (show) { + UriHandler.browse(url) + } + } + + fun executeJs(browserId: Int, code: String) { + val panel = getOrCreate(browserId) + + panel.executeJs(code) + } + + override fun createComponent(componentId: Int) = EmbeddedBrowserPanel(componentId, openInExternalBrowser) + +} diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/component/MarkdownPanelManager.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/component/MarkdownPanelManager.kt index 1311cc9aa..7bac52c79 100644 --- a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/component/MarkdownPanelManager.kt +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/component/MarkdownPanelManager.kt @@ -23,153 +23,27 @@ */ package org.jetbrains.projector.client.web.component -import kotlinx.browser.document import kotlinx.dom.clear -import org.jetbrains.projector.common.misc.Do -import org.jetbrains.projector.common.protocol.data.CommonIntSize -import org.jetbrains.projector.common.protocol.data.Point import org.jetbrains.projector.util.logging.Logger import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLIFrameElement import org.w3c.dom.Node import org.w3c.dom.get import org.w3c.dom.parsing.DOMParser -import kotlin.collections.component1 -import kotlin.collections.component2 import kotlin.math.absoluteValue -class MarkdownPanelManager(private val zIndexByWindowIdGetter: (Int) -> Int?, private val openInExternalBrowser: (String) -> Unit) { +class MarkdownPanelManager( + zIndexByWindowIdGetter: (Int) -> Int?, + private val openInExternalBrowser: (String) -> Unit, +) : ClientComponentManager(zIndexByWindowIdGetter) { - private class MarkdownPanel(openInExternalBrowser: (String) -> Unit) { + class MarkdownPanel(id: Int, openInExternalBrowser: (String) -> Unit) : ClientComponent(id) { - val iFrame: HTMLIFrameElement = createIFrame(openInExternalBrowser) - - var windowId: Int? = null - - fun dispose() { - iFrame.remove() - } - - companion object { - - private fun createIFrame(openInExternalBrowser: (String) -> Unit) = (document.createElement("iframe") as HTMLIFrameElement).apply { - style.apply { - position = "fixed" - backgroundColor = "#FFF" - overflowX = "scroll" - overflowY = "scroll" - display = "none" - } - - frameBorder = "0" - - document.body!!.appendChild(this) - - // cancel auto-started load of about:blank in Firefox - // https://stackoverflow.com/questions/7828502/cannot-set-document-body-innerhtml-of-iframe-in-firefox - contentDocument!!.apply { - open() - close() - } - - contentDocument!!.oncontextmenu = { false } - - // adopted from processLinks.js - contentDocument!!.onclick = { e -> - var target = e.target.asDynamic() - while (target != null && target.tagName != "A") { - target = target.parentNode - } - - if (target == null) { - true - } - else if (target.tagName == "A" && target.hasAttribute("href").unsafeCast()) { - e.stopPropagation() - - val href = target.getAttribute("href").unsafeCast() - if (href[0] == '#') { - val elementId = href.substring(1) - contentDocument!!.getElementById(elementId)?.scrollIntoView() - } - else { - openInExternalBrowser(href) - } - - false - } - else { - null - } - } - } - } - } - - private val idToPanel = mutableMapOf() - - private fun getOrCreate(markdownPanelId: Int): MarkdownPanel { - return idToPanel.getOrPut(markdownPanelId) { MarkdownPanel(openInExternalBrowser) } - } - - fun placeToWindow(markdownPanelId: Int, windowId: Int) { - val panel = getOrCreate(markdownPanelId) - - panel.windowId = windowId - - val zIndex = zIndexByWindowIdGetter(windowId) ?: return - - panel.iFrame.style.zIndex = (zIndex + 1).toString() - } - - fun updatePlacements() { - idToPanel.forEach { (markdownPanelId, panel) -> - panel.windowId?.let { placeToWindow(markdownPanelId, it) } + init { + setLinkProcessor(openInExternalBrowser) } } - fun show(markdownPanelId: Int, show: Boolean) { - val panel = getOrCreate(markdownPanelId) - - Do exhaustive when (show) { - true -> panel.iFrame.style.display = "block" - - false -> panel.iFrame.style.display = "none" - } - } - - fun move(markdownPanelId: Int, point: Point) { - val panel = getOrCreate(markdownPanelId) - - panel.iFrame.style.apply { - left = "${point.x}px" - top = "${point.y}px" - } - } - - fun resize(markdownPanelId: Int, size: CommonIntSize) { - val panel = getOrCreate(markdownPanelId) - - panel.iFrame.style.apply { - width = "${size.width}px" - height = "${size.height}px" - } - } - - fun dispose(markdownPanelId: Int) { - val panel = getOrCreate(markdownPanelId) - - panel.dispose() - - idToPanel.remove(markdownPanelId) - } - - fun disposeAll() { - idToPanel.values.forEach { it.dispose() } - - idToPanel.clear() - } - fun setHtml(markdownPanelId: Int, html: String) { val panel = getOrCreate(markdownPanelId) @@ -217,6 +91,8 @@ class MarkdownPanelManager(private val zIndexByWindowIdGetter: (Int) -> Int?, pr } } + override fun createComponent(componentId: Int) = MarkdownPanel(componentId, openInExternalBrowser) + companion object { private val logger = Logger() diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/protocol/ManualJsonToClientMessageDecoder.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/protocol/ManualJsonToClientMessageDecoder.kt index fd3073d99..6e740835a 100644 --- a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/protocol/ManualJsonToClientMessageDecoder.kt +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/protocol/ManualJsonToClientMessageDecoder.kt @@ -67,6 +67,13 @@ object ManualJsonToClientMessageDecoder : ToClientMessageDecoder { "n" -> ServerMarkdownEvent.ServerMarkdownScrollEvent(content["a"] as Int, content["b"] as Int) "o" -> ServerBrowseUriEvent(content["a"] as String) "p" -> ServerWindowColorsEvent(content["a"].unsafeCast().toColorsStorage()) + "q" -> ServerBrowserEvent.ExecuteJsEvent(content["a"] as Int, content["b"] as String, content["c"] as String?, content["d"] as Int) + "r" -> ServerBrowserEvent.SetHtmlEvent(content["a"] as Int, content["b"] as String) + "s" -> ServerBrowserEvent.LoadUrlEvent(content["a"] as Int, content["b"] as String, content["c"] as Boolean) + "t" -> ServerBrowserEvent.ShowEvent(content["a"] as Int, content["b"] as Boolean, content["c"] as? Int) + "u" -> ServerBrowserEvent.MoveEvent(content["a"] as Int, content["b"].unsafeCast().toPoint()) + "v" -> ServerBrowserEvent.ResizeEvent(content["a"] as Int, content["b"].unsafeCast().toCommonIntSize()) + "w" -> ServerBrowserEvent.SetOpenLinksInExternalBrowserEvent(content["a"] as Int, content["b"] as Boolean) else -> throw IllegalArgumentException("Unsupported event type: ${JSON.stringify(this)}") } } diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/state/ClientAction.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/state/ClientAction.kt index 2a8bc1ecb..7c91a51e3 100644 --- a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/state/ClientAction.kt +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/state/ClientAction.kt @@ -26,6 +26,7 @@ package org.jetbrains.projector.client.web.state import org.jetbrains.projector.client.web.WindowSizeController import org.jetbrains.projector.common.protocol.toServer.ClientEvent +@JsExport sealed class ClientAction { class Start( diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/state/ClientState.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/state/ClientState.kt index 3bc4cc8fe..993b7885a 100644 --- a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/state/ClientState.kt +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/state/ClientState.kt @@ -38,6 +38,7 @@ import org.jetbrains.projector.client.common.protocol.KotlinxJsonToClientHandsha import org.jetbrains.projector.client.common.protocol.KotlinxJsonToServerHandshakeEncoder import org.jetbrains.projector.client.web.ServerEventsProcessor import org.jetbrains.projector.client.web.WindowSizeController +import org.jetbrains.projector.client.web.component.EmbeddedBrowserManager import org.jetbrains.projector.client.web.component.MarkdownPanelManager import org.jetbrains.projector.client.web.debug.DivSentReceivedBadgeShower import org.jetbrains.projector.client.web.debug.NoSentReceivedBadgeShower @@ -411,6 +412,10 @@ sealed class ClientState { stateMachine.fire(ClientAction.AddEvent(ClientOpenLinkEvent(link))) } + private val embeddedBrowserManager = EmbeddedBrowserManager(windowManager::getWindowZIndex) { link -> + stateMachine.fire(ClientAction.AddEvent(ClientOpenLinkEvent(link))) + } + private val closeBlocker = when (ParamsProvider.BLOCK_CLOSING) { true -> CloseBlockerImpl(window) false -> NopCloseBlocker @@ -440,7 +445,7 @@ sealed class ClientState { val decompressTimeStamp = TimeStamp.current val commands = decoder.decode(decompressed) val decodeTimestamp = TimeStamp.current - serverEventsProcessor.process(commands, pingStatistics, typing, markdownPanelManager, inputController) + serverEventsProcessor.process(commands, pingStatistics, typing, markdownPanelManager, embeddedBrowserManager, inputController) val drawTimestamp = TimeStamp.current imageCacher.collectGarbage() @@ -558,6 +563,7 @@ sealed class ClientState { windowSizeController.removeListener() typing.dispose() markdownPanelManager.disposeAll() + embeddedBrowserManager.disposeAll() closeBlocker.removeListener() selectionBlocker.unblockSelection() connectionWatcher.removeWatcher() @@ -592,6 +598,7 @@ sealed class ClientState { return WaitingOpening(stateMachine, newConnection, windowSizeController, layers) { windowDataEventsProcessor.onClose() markdownPanelManager.disposeAll() + embeddedBrowserManager.disposeAll() closeBlocker.removeListener() selectionBlocker.unblockSelection() layers.reconnectionMessageUpdater(null) diff --git a/projector-common/build.gradle.kts b/projector-common/build.gradle.kts index c332dca7c..2d1327e12 100644 --- a/projector-common/build.gradle.kts +++ b/projector-common/build.gradle.kts @@ -31,6 +31,7 @@ plugins { setupJacoco(isKotlinMpModule = true) +val intellijPlatformVersion: String by project val kotlinVersion: String by project val serializationVersion: String by project @@ -69,6 +70,10 @@ kotlin { } val jvmMain by getting { + dependencies { + implementation(project(":projector-util-loading")) + compileOnly("com.jetbrains.intellij.platform:core-impl:$intellijPlatformVersion") + } } val jvmTest by getting { diff --git a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/data/Point.kt b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/data/Point.kt index 0d6ddb290..947f0eaaf 100644 --- a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/data/Point.kt +++ b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/data/Point.kt @@ -32,4 +32,7 @@ data class Point( val x: Double, @SerialName("b") val y: Double, -) +) { + + constructor(x: Int, y: Int) : this(x.toDouble(), y.toDouble()) +} diff --git a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/handshake/Constant.kt b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/handshake/Constant.kt index 043238f8b..7654d8540 100644 --- a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/handshake/Constant.kt +++ b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/handshake/Constant.kt @@ -63,4 +63,5 @@ val commonVersionList = listOf( -1867356666, 1741530029, 2040465192, + 2043769487, ) diff --git a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toClient/ServerEvent.kt b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toClient/ServerEvent.kt index 647187c7b..0877d355e 100644 --- a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toClient/ServerEvent.kt +++ b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toClient/ServerEvent.kt @@ -242,3 +242,78 @@ data class ServerWindowColorsEvent( val windowHeaderInactiveText: PaintValue.Color, ) } + +@Serializable +sealed class ServerBrowserEvent : ServerEvent() { + + @Serializable + @SerialName("q") + data class ExecuteJsEvent( + @SerialName("a") + val browserId: Int, + @SerialName("b") + val code: String, + @SerialName("c") + val url: String?, + @SerialName("d") + val line: Int, + ) : ServerBrowserEvent() + + @Serializable + @SerialName("r") + data class SetHtmlEvent( + @SerialName("a") + val browserId: Int, + @SerialName("b") + val html: String, + ) : ServerBrowserEvent() + + @Serializable + @SerialName("s") + data class LoadUrlEvent( + @SerialName("a") + val browserId: Int, + @SerialName("b") + val url: String, + @SerialName("c") + val show: Boolean, + ) : ServerBrowserEvent() + + @Serializable + @SerialName("t") + data class ShowEvent( + @SerialName("a") + val browserId: Int, + @SerialName("b") + val show: Boolean, + @SerialName("c") + val windowId: Int?, + ) : ServerBrowserEvent() + + @Serializable + @SerialName("u") + data class MoveEvent( + @SerialName("a") + val browserId: Int, + @SerialName("b") + val position: Point, + ) : ServerBrowserEvent() + + @Serializable + @SerialName("v") + data class ResizeEvent( + @SerialName("a") + val browserId: Int, + @SerialName("b") + val size: CommonIntSize, + ) : ServerBrowserEvent() + + @Serializable + @SerialName("w") + data class SetOpenLinksInExternalBrowserEvent( + @SerialName("a") + val browserId: Int, + @SerialName("b") + val openLinksInExternalBrowser: Boolean, + ) : ServerBrowserEvent() +} diff --git a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toServer/ClientEvent.kt b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toServer/ClientEvent.kt index 90c869c34..61c42e40d 100644 --- a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toServer/ClientEvent.kt +++ b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toServer/ClientEvent.kt @@ -26,6 +26,8 @@ package org.jetbrains.projector.common.protocol.toServer import kotlinx.serialization.Serializable import org.jetbrains.projector.common.protocol.data.* import org.jetbrains.projector.common.protocol.handshake.DisplayDescription +import kotlin.js.ExperimentalJsExport +import kotlin.js.JsExport enum class ResizeDirection { NW, @@ -239,3 +241,12 @@ data class ClientNotificationEvent( val message: String, val notificationType: ClientNotificationType, ) : ClientEvent() + +@OptIn(ExperimentalJsExport::class) +@JsExport +@Serializable +data class ClientJcefEvent( + val browserId: Int, + val functionName: String, + val data: String, +) : ClientEvent() diff --git a/projector-common/src/jvmMain/kotlin/org/jetbrains/projector/common/EventSender.kt b/projector-common/src/jvmMain/kotlin/org/jetbrains/projector/common/EventSender.kt new file mode 100644 index 000000000..2b2548663 --- /dev/null +++ b/projector-common/src/jvmMain/kotlin/org/jetbrains/projector/common/EventSender.kt @@ -0,0 +1,44 @@ +/* + * MIT License + * + * Copyright (c) 2019-2022 JetBrains s.r.o. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jetbrains.projector.common + +import org.jetbrains.projector.common.event.ServerEventPart +import org.jetbrains.projector.common.protocol.toClient.ServerEvent +import org.jetbrains.projector.util.loading.ProjectorClassLoader +import java.util.* + +interface EventSender { + + fun sendEvent(event: ServerEvent) + + fun sendEventPart(part: ServerEventPart) + + companion object { + + val instance: EventSender by lazy { + // Use ProjectorClassLoader because ProjectorServer is instantiated by it and in CommonQueueEvenSender we access server + ServiceLoader.load(EventSender::class.java, ProjectorClassLoader.instance).findFirst().orElseThrow() + } + } +} diff --git a/projector-common/src/jvmMain/kotlin/org/jetbrains/projector/common/event/ServerEventPart.kt b/projector-common/src/jvmMain/kotlin/org/jetbrains/projector/common/event/ServerEventPart.kt new file mode 100644 index 000000000..925df2a6c --- /dev/null +++ b/projector-common/src/jvmMain/kotlin/org/jetbrains/projector/common/event/ServerEventPart.kt @@ -0,0 +1,34 @@ +/* + * MIT License + * + * Copyright (c) 2019-2022 JetBrains s.r.o. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jetbrains.projector.common.event + +import java.awt.Component + +sealed class ServerEventPart + +data class BrowserShowEventPart( + val browserId: Int, + val show: Boolean, + val component: Component?, +) : ServerEventPart() diff --git a/projector-common/src/jvmMain/kotlin/org/jetbrains/projector/common/intellij/Compatibility.kt b/projector-common/src/jvmMain/kotlin/org/jetbrains/projector/common/intellij/Compatibility.kt new file mode 100644 index 000000000..a299650b4 --- /dev/null +++ b/projector-common/src/jvmMain/kotlin/org/jetbrains/projector/common/intellij/Compatibility.kt @@ -0,0 +1,57 @@ +/* + * MIT License + * + * Copyright (c) 2019-2022 JetBrains s.r.o. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +@file:UseProjectorLoader + +package org.jetbrains.projector.common.intellij + +import com.intellij.openapi.application.ApplicationInfo +import com.intellij.openapi.util.BuildNumber +import org.jetbrains.projector.util.loading.UseProjectorLoader + +fun buildAtLeast(version: String) = ApplicationInfo.getInstance().build.buildAtLeast(version) + +@Suppress("unused") +fun buildLowerThan(version: String) = ApplicationInfo.getInstance().build.buildLowerThan(version) + +@Suppress("unused") +fun buildInRange(start: String, endExclusive: String) = ApplicationInfo.getInstance().build.buildInRange(start, endExclusive) + +fun BuildNumber.buildAtLeast(version: String): Boolean { + val versionToCheck = BuildNumber.fromString(version) ?: throw IllegalArgumentException("Invalid version string $version") + + return this >= versionToCheck +} + +fun BuildNumber.buildLowerThan(version: String): Boolean { + val versionToCheck = BuildNumber.fromString(version) ?: throw IllegalArgumentException("Invalid version string $version") + + return this < versionToCheck +} + +fun BuildNumber.buildInRange(start: String, endExclusive: String): Boolean { + val startVersion = BuildNumber.fromString(start) ?: throw IllegalArgumentException("Invalid version string $start") + val endExclusiveVersion = BuildNumber.fromString(endExclusive) ?: throw IllegalArgumentException("Invalid version string $endExclusive") + + return startVersion <= this && this < endExclusiveVersion +} diff --git a/projector-ij-common/build.gradle.kts b/projector-ij-common/build.gradle.kts index ed0c6092e..98e3156e2 100644 --- a/projector-ij-common/build.gradle.kts +++ b/projector-ij-common/build.gradle.kts @@ -35,14 +35,21 @@ publishToSpace("java") setupJacoco() +val intellijJcefVersion: String by project val intellijPlatformVersion: String by project val kotestVersion: String by project +val jsoupVersion: String by project val kotlinVersion: String by project dependencies { + implementation(project(":projector-common")) implementation(project(":projector-util-loading")) + implementation("org.jsoup:jsoup:$jsoupVersion") + compileOnly("com.jetbrains.intellij.platform:core-impl:$intellijPlatformVersion") + compileOnly("com.jetbrains.intellij.platform:util-ui:$intellijPlatformVersion") + compileOnly("org.jetbrains.intellij.deps.jcef:jcef:$intellijJcefVersion") testImplementation("io.kotest:kotest-assertions-core:$kotestVersion") testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion") diff --git a/projector-ij-common/src/main/kotlin/org/jetbrains/projector/ij/jcef/BrowserState.kt b/projector-ij-common/src/main/kotlin/org/jetbrains/projector/ij/jcef/BrowserState.kt new file mode 100644 index 000000000..a3aca4ca4 --- /dev/null +++ b/projector-ij-common/src/main/kotlin/org/jetbrains/projector/ij/jcef/BrowserState.kt @@ -0,0 +1,37 @@ +/* + * MIT License + * + * Copyright (c) 2019-2022 JetBrains s.r.o. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jetbrains.projector.ij.jcef + +internal data class BrowserState( + val html: String, + val executedJs: List, + val openInExternalBrowser: Boolean, + val externalUrl: String, +) + +internal data class JsCode( + val code: String, + val url: String? = null, + val line: Int = 0, +) diff --git a/projector-ij-common/src/main/kotlin/org/jetbrains/projector/ij/jcef/CefHandlers.kt b/projector-ij-common/src/main/kotlin/org/jetbrains/projector/ij/jcef/CefHandlers.kt new file mode 100644 index 000000000..124972ef3 --- /dev/null +++ b/projector-ij-common/src/main/kotlin/org/jetbrains/projector/ij/jcef/CefHandlers.kt @@ -0,0 +1,92 @@ +/* + * MIT License + * + * Copyright (c) 2019-2022 JetBrains s.r.o. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jetbrains.projector.ij.jcef + +import org.cef.CefClient +import org.cef.browser.CefMessageRouter +import org.cef.handler.CefClientHandler +import org.cef.handler.CefLifeSpanHandler +import org.cef.handler.CefMessageRouterHandler +import org.jetbrains.projector.util.loading.UseProjectorLoader + +@UseProjectorLoader +public object CefHandlers { + + @JvmStatic + private val handlers = mutableMapOf>() + + @JvmStatic + public fun addMessageRouterHandler(router: CefMessageRouter, handler: CefMessageRouterHandler, first: Boolean): Boolean { + handlers.addCreateIfNeeded(router, handler, first) + return true + } + + @JvmStatic + public fun removeMessageRouterHandler(router: CefMessageRouter, handler: CefMessageRouterHandler): Boolean { + return handlers[router]?.remove(handler) ?: false + } + + @JvmStatic + public fun clearMessageRouterHandlers(router: CefMessageRouter) { + handlers.remove(router) + } + + internal fun getRouterHandlers(router: CefMessageRouter): List = handlers[router] ?: emptyList() + + //=================================================================================== + + @JvmStatic + private val routers = mutableMapOf>() + + @JvmStatic + public fun onMessageRouterAdded(handler: CefClientHandler, router: CefMessageRouter) { + routers.addCreateIfNeeded(handler, router) + } + + @JvmStatic + public fun onMessageRouterRemoved(handler: CefClientHandler, router: CefMessageRouter) { + routers[handler]?.remove(router) + } + + internal fun getMessageRouters(handler: CefClientHandler): List = routers[handler] ?: emptyList() + + //=================================================================================== + + @JvmStatic + public fun onLifeSpanHandlerAdded(client: CefClient, handler: CefLifeSpanHandler) { + ProjectorCefBrowser.getClientInstances(client).forEach { it.onLifeSpanHandlerAdded(handler) } + } + + @JvmStatic + public fun onLifeSpanHandlerRemoved(client: CefClient) { + ProjectorCefBrowser.getClientInstances(client).forEach { it.onLifeSpanHandlerRemoved() } + } + + //=================================================================================== + + private fun MutableMap>.addCreateIfNeeded(key: K, item: T, first: Boolean = false) { + val list = getOrPut(key) { mutableListOf() } + if (first) list.add(0, item) else list.add(item) + } +} diff --git a/projector-ij-common/src/main/kotlin/org/jetbrains/projector/ij/jcef/CefHelpers.kt b/projector-ij-common/src/main/kotlin/org/jetbrains/projector/ij/jcef/CefHelpers.kt new file mode 100644 index 000000000..a79604c95 --- /dev/null +++ b/projector-ij-common/src/main/kotlin/org/jetbrains/projector/ij/jcef/CefHelpers.kt @@ -0,0 +1,70 @@ +/* + * MIT License + * + * Copyright (c) 2019-2022 JetBrains s.r.o. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +@file:UseProjectorLoader + +package org.jetbrains.projector.ij.jcef + +import org.cef.browser.CefMessageRouter +import org.cef.callback.CefQueryCallback +import org.cef.handler.CefClientHandler +import org.cef.handler.CefMessageRouterHandler +import org.jetbrains.projector.common.intellij.buildAtLeast +import org.jetbrains.projector.util.loading.UseProjectorLoader +import org.jetbrains.projector.util.loading.state.IdeState + +public fun CefClientHandler.getMessageRouters(): List { + return CefHandlers.getMessageRouters(this) +} + +@Suppress("unused") // used in server +public fun CefMessageRouter.getHandlers(): List { + return CefHandlers.getRouterHandlers(this) +} + +@Suppress("unused") // used in server +public fun CefMessageRouterHandler.onProjectorQuery(projectorCefBrowser: ProjectorCefBrowser, query: String) { + onQuery(projectorCefBrowser, DEFAULT_FRAME, 0, query, false, DEFAULT_CALLBACK) +} + +internal val DEFAULT_FRAME by lazy { ProjectorCefFrame() } + +private val DEFAULT_CALLBACK by lazy { + object : CefQueryCallback { + override fun success(response: String?) { + // TODO + } + + override fun failure(error_code: Int, error_message: String?) { + // TODO + } + } +} + +public fun isCefAvailable(): Boolean = IdeState.isIdeAttached && buildAtLeast("202") + +@Suppress("unused") // used in server +public fun updateCefBrowsersSafely() { + if (!isCefAvailable()) return + ProjectorCefBrowser.updateAll() +} diff --git a/projector-ij-common/src/main/kotlin/org/jetbrains/projector/ij/jcef/ProjectorCefBrowser.kt b/projector-ij-common/src/main/kotlin/org/jetbrains/projector/ij/jcef/ProjectorCefBrowser.kt new file mode 100644 index 000000000..9734933ad --- /dev/null +++ b/projector-ij-common/src/main/kotlin/org/jetbrains/projector/ij/jcef/ProjectorCefBrowser.kt @@ -0,0 +1,608 @@ +/* + * MIT License + * + * Copyright (c) 2019-2022 JetBrains s.r.o. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jetbrains.projector.ij.jcef + +import org.cef.CefClient +import org.cef.browser.CefBrowser +import org.cef.browser.CefFrame +import org.cef.callback.* +import org.cef.handler.* +import org.cef.misc.CefPdfPrintSettings +import org.cef.network.CefRequest +import org.jetbrains.projector.common.EventSender +import org.jetbrains.projector.common.event.BrowserShowEventPart +import org.jetbrains.projector.common.protocol.data.CommonIntSize +import org.jetbrains.projector.common.protocol.toClient.ServerBrowserEvent +import org.jetbrains.projector.common.protocol.toServer.ClientJcefEvent +import org.jetbrains.projector.util.loading.UseProjectorLoader +import org.jetbrains.projector.util.logging.Logger +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import org.jsoup.parser.Tag +import java.awt.* +import java.awt.event.* +import java.awt.image.BufferedImage +import java.io.BufferedReader +import java.io.InputStreamReader +import java.lang.invoke.MethodHandles +import java.net.URL +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicInteger +import javax.swing.* +import javax.swing.event.AncestorEvent +import javax.swing.event.AncestorListener +import org.jetbrains.projector.common.protocol.data.Point as PrjPoint + +@UseProjectorLoader +public class ProjectorCefBrowser @JvmOverloads constructor( + public val cefClient: CefClient, + url: String? = null, + public val originalBrowser: CefBrowser? = null, +) : CefNativeAdapter(), CefBrowser { + + private var state = BrowserState("", emptyList(), false, "") + + private val id = ID_COUNTER.incrementAndGet() + + private val lifeSpanHandlers = mutableListOf() + + private val headlessBackingComponent = JPanel().apply { + val header = JLabel("If you see this then current IDE version is not supported") + add(header, BorderLayout.NORTH) + } + + private fun sendMoveResizeEvent() { + sendMoveEvent() + sendEvent(ServerBrowserEvent.ResizeEvent(id, CommonIntSize(uiComponent.width, uiComponent.height))) + } + + private fun sendMoveEvent() { + if (!uiComponent.isShowing) return + + var parent = uiComponent + var previousParent = parent + while (parent !is Window) { + previousParent = parent + parent = parent.parent + } + + val locationInWindow = SwingUtilities.convertPoint(uiComponent, uiComponent.location, parent) + + // in agent mode rootPane for some reason has vertical offset of about 30 pixels (window header height?) + val realY = locationInWindow.y - previousParent.y + sendEvent(ServerBrowserEvent.MoveEvent(id, PrjPoint(locationInWindow.x, realY))) + } + + private fun sendShowEvent(shown: Boolean?) { + val valueToSend = when (shown) { + null -> uiComponent.isShowing + else -> shown + } + eventSender.sendEventPart(BrowserShowEventPart(id, valueToSend, uiComponent.takeIf { valueToSend })) + } + + private fun sendJs(code: String, url: String? = null, line: Int = 0) { + if (state.html.isEmpty()) { + logger.error(IllegalStateException("Sending js before html is not supported")) { "js code: ${code.take(200)}" } + return + } + + if (code.contains("const render = ;")) { + return // TODO seems like markdown bug + } + sendEvent(ServerBrowserEvent.ExecuteJsEvent(id, code, url, line)) + } + + private fun sendHtml(html: String) { + sendEvent(ServerBrowserEvent.SetHtmlEvent(id, html)) + } + + private fun sendSetOpenLinksInExternalBrowser(openLinksInExternalBrowser: Boolean) { + sendEvent(ServerBrowserEvent.SetOpenLinksInExternalBrowserEvent(id, openLinksInExternalBrowser)) + } + + private fun ifHeadless(foo: () -> Unit) { + if (originalBrowser == null) { + foo() + } + } + + init { + instances[id] = this + + uiComponent.addComponentListener(object : ComponentAdapter() { + + override fun componentResized(event: ComponentEvent) { + sendMoveResizeEvent() + } + + override fun componentMoved(e: ComponentEvent?) { + sendMoveEvent() + } + }) + + (uiComponent as? JComponent)?.addAncestorListener(object : AncestorListener { + + override fun ancestorAdded(event: AncestorEvent?) { + instances[id] = this@ProjectorCefBrowser + + sendMoveResizeEvent() + sendShowEvent(true) + // in Python Packages browser created and this option is set before component is attached, so resendState won't be called + if (state.openInExternalBrowser) { + sendSetOpenLinksInExternalBrowser(true) + } + } + + override fun ancestorRemoved(event: AncestorEvent?) { + sendShowEvent(false) + } + + override fun ancestorMoved(event: AncestorEvent?) { + sendMoveEvent() + } + }) + + loadURL(url) + } + + override fun getNativeRef(identifer: String?): Long { + return (originalBrowser as? CefNative)?.getNativeRef(identifer) ?: super.getNativeRef(identifer) + } + + override fun setNativeRef(identifer: String?, nativeRef: Long) { + (originalBrowser as? CefNative)?.setNativeRef(identifer, nativeRef) + } + + override fun createImmediately() { + originalBrowser?.createImmediately() + } + + override fun getUIComponent(): Component { + return originalBrowser?.uiComponent ?: headlessBackingComponent + } + + override fun getClient(): CefClient = cefClient + + override fun getRenderHandler(): CefRenderHandler { + return originalBrowser?.renderHandler ?: TODO("Not yet implemented") + } + + override fun getWindowHandler(): CefWindowHandler { + return originalBrowser?.windowHandler ?: TODO("Not yet implemented") + } + + override fun canGoBack(): Boolean { + return originalBrowser?.canGoBack() ?: false + } + + override fun goBack() { + originalBrowser?.goBack() + } + + override fun canGoForward(): Boolean { + return originalBrowser?.canGoForward() ?: false + } + + override fun goForward() { + originalBrowser?.goForward() + } + + override fun isLoading(): Boolean { + return originalBrowser?.isLoading ?: false + } + + override fun reload() { + originalBrowser?.reload() + } + + override fun reloadIgnoreCache() { + originalBrowser?.reloadIgnoreCache() + } + + override fun stopLoad() { + originalBrowser?.stopLoad() + } + + override fun getIdentifier(): Int { + return originalBrowser?.identifier ?: id + } + + override fun getMainFrame(): CefFrame { + return originalBrowser?.mainFrame ?: TODO("Not yet implemented") + } + + override fun getFocusedFrame(): CefFrame { + return originalBrowser?.focusedFrame ?: TODO("Not yet implemented") + } + + override fun getFrame(identifier: Long): CefFrame { + return originalBrowser?.getFrame(identifier) ?: TODO("Not yet implemented") + } + + override fun getFrame(name: String?): CefFrame { + return originalBrowser?.getFrame(name) ?: TODO("Not yet implemented") + } + + override fun getFrameIdentifiers(): Vector { + return originalBrowser?.frameIdentifiers ?: Vector() + } + + override fun getFrameNames(): Vector { + return originalBrowser?.frameNames ?: Vector() + } + + override fun getFrameCount(): Int { + return originalBrowser?.frameCount ?: 0 + } + + override fun isPopup(): Boolean { + return originalBrowser?.isPopup ?: false + } + + override fun hasDocument(): Boolean { + return originalBrowser?.hasDocument() ?: true + } + + override fun viewSource() { + originalBrowser?.viewSource() + } + + override fun getSource(visitor: CefStringVisitor?) { + originalBrowser?.getSource(visitor) + } + + override fun getText(visitor: CefStringVisitor?) { + originalBrowser?.getText(visitor) + } + + override fun loadRequest(request: CefRequest?) { + originalBrowser?.loadRequest(request) + } + + override fun loadURL(url: String?) { + originalBrowser?.loadURL(url) + + if (url.isNullOrEmpty() || url == "about:blank") return + + val parsedUrl = URL(url) + + if (parsedUrl.protocol == "file") { + // CEF allow only urls, so in Intellij platform there is a workaround to support direct rendering of html + val html = getHtmlMap(this)?.get(url) + if (html != null) { + loadHtml(html) + return + } + } + + if (parsedUrl.protocol.startsWith("http") && parsedUrl.host != "localhost") { + beforeLoad() + state = state.copy(externalUrl = url) + sendEvent(ServerBrowserEvent.LoadUrlEvent(id, url, true)) + afterLoad() + return + } + + val result = readUrlAsText(url) ?: run { + val code = CefLoadHandler.ErrorCode.ERR_CONNECTION_FAILED + cefClient.onLoadError(this, DEFAULT_FRAME, code, "Cannot read url content", url) + return + } + loadHtml(result) + } + + private fun F.tryOrNull(callable: (F) -> T) = try { + callable(this) + } + catch (_: Throwable) { + null + } + + private fun readUrlAsText(url: String) = tryOrNull { URL(url) }?.openConnection()?.tryOrNull { connection -> + val reader = BufferedReader(InputStreamReader(connection.inputStream)) + reader.readText() + } + + /** + * JS functions to communicate with IDE + */ + private fun getRequiredJs() = cefClient.getMessageRouters().mapNotNull { + val jsQueryFunctionName = it.messageRouterConfig?.jsQueryFunction ?: return@mapNotNull null + + @Suppress("JSUnresolvedVariable", "JSUnresolvedFunction") + // language=js + """ + window.$jsQueryFunctionName = function(obj) { + const prjWebModule = parent["projector-client-web"]; + const prjWebPackage = prjWebModule.org.jetbrains.projector.client.web; + const addEvent = prjWebPackage.state.ClientAction.AddEvent; + const jcefEvent = prjWebModule.${ClientJcefEvent::class.java.name}; + prjWebPackage.Application.fireAction(new addEvent(new jcefEvent($id, '$jsQueryFunctionName', obj.request))); + } + """ + } + + private fun prepareHtml(html: String): String { + val doc = Jsoup.parse(html) + doc.head().apply { + + // inline css + getElementsByTag("link").forEach { + val href = it.attr("abs:href") + if (href.isNotEmpty() && it.attr("rel") == "stylesheet") { + val styles = readUrlAsText(href) ?: return@forEach + + val link = Element(Tag.valueOf("style"), "") + .text(styles) + .attr("type", "text/css") + + it.replaceWith(link) + } + } + + // inline js + getElementsByTag("script").forEach { + val src = it.attr("src") + if (src.isNotEmpty()) { + val script = readUrlAsText(src) ?: return@forEach + it.removeAttr("src") + it.append(script) + } + } + + // allow all resources to support inlined css and js + getElementsByTag("meta").forEach { + if (it.attr("http-equiv") == "Content-Security-Policy") { + it.remove() + } + } + } + + val correctedHtml = doc.toString() + + return correctedHtml + .replace( + "", + """ + + ${getRequiredJs().joinToString("\n") { "" }} + """.trimIndent() + ) + } + + private fun loadHtml(html: String) { + if (html.isEmpty()) return + + beforeLoad() + + val preparedHtml = prepareHtml(html) + val executedJs = if (state.html.isEmpty()) state.executedJs else emptyList() + state = state.copy(html = preparedHtml, executedJs = executedJs) + + sendHtml(preparedHtml) + executedJs.forEach { (code, url, line) -> + sendJs(code, url, line) // send all code that tried to be sent before html + } + + afterLoad() + } + + private fun beforeLoad() { + // TODO research what should be called before load + //cefClient.onLoadStart(this, DEFAULT_FRAME, CefRequest.TransitionType.TT_EXPLICIT) // calls methods in frame, but seems not obligatory + } + + private fun afterLoad() = ifHeadless { + cefClient.onLoadingStateChange(this, false, false, false) + cefClient.onLoadEnd(this, DEFAULT_FRAME, 200) + } + + override fun executeJavaScript(code: String?, url: String?, line: Int) { + originalBrowser?.executeJavaScript(code, url, line) + + if (code == null) return + state = state.copy(executedJs = state.executedJs + JsCode(code, url, line)) + sendJs(code, url, line) + } + + override fun getURL(): String { + return originalBrowser?.url ?: "https://example.com" + } + + override fun close(force: Boolean) { + originalBrowser?.close(force) + instances.remove(id) + } + + override fun setCloseAllowed() { + originalBrowser?.setCloseAllowed() + } + + override fun doClose(): Boolean { + instances.remove(id) + return originalBrowser?.doClose() ?: true + } + + override fun onBeforeClose() { + originalBrowser?.onBeforeClose() + } + + override fun setFocus(enable: Boolean) { + originalBrowser?.setFocus(enable) + } + + override fun setWindowVisibility(visible: Boolean) { + originalBrowser?.setWindowVisibility(visible) + } + + override fun getZoomLevel(): Double { + return originalBrowser?.zoomLevel ?: 1.0 + } + + override fun setZoomLevel(zoomLevel: Double) { + originalBrowser?.zoomLevel = zoomLevel + } + + override fun runFileDialog( + mode: CefDialogHandler.FileDialogMode?, + title: String?, + defaultFilePath: String?, + acceptFilters: Vector?, + selectedAcceptFilter: Int, + callback: CefRunFileDialogCallback?, + ) { + originalBrowser?.runFileDialog(mode, title, defaultFilePath, acceptFilters, selectedAcceptFilter, callback) + } + + override fun startDownload(url: String?) { + originalBrowser?.startDownload(url) + } + + override fun print() { + originalBrowser?.print() + } + + override fun printToPDF(path: String?, settings: CefPdfPrintSettings?, callback: CefPdfPrintCallback?) { + originalBrowser?.printToPDF(path, settings, callback) + } + + override fun find(identifier: Int, searchText: String?, forward: Boolean, matchCase: Boolean, findNext: Boolean) { + originalBrowser?.find(identifier, searchText, forward, matchCase, findNext) + } + + override fun stopFinding(clearSelection: Boolean) { + originalBrowser?.stopFinding(clearSelection) + } + + override fun getDevTools(): CefBrowser { + return originalBrowser?.devTools ?: TODO("Not yet implemented") + } + + override fun getDevTools(inspectAt: Point?): CefBrowser { + return originalBrowser?.getDevTools(inspectAt) ?: TODO("Not yet implemented") + } + + override fun replaceMisspelling(word: String?) { + originalBrowser?.replaceMisspelling(word) + } + + override fun wasResized(width: Int, height: Int) { + originalBrowser?.wasResized(width, height) + } + + override fun sendKeyEvent(e: KeyEvent?) { + originalBrowser?.sendKeyEvent(e) + } + + override fun sendMouseEvent(e: MouseEvent?) { + originalBrowser?.sendMouseEvent(e) + } + + override fun sendMouseWheelEvent(e: MouseWheelEvent?) { + originalBrowser?.sendMouseWheelEvent(e) + } + + override fun createScreenshot(nativeResolution: Boolean): CompletableFuture { + return originalBrowser?.createScreenshot(nativeResolution) ?: TODO("Not yet implemented") + } + + override fun equals(other: Any?): Boolean { + return originalBrowser?.equals(other) ?: super.equals(other) + } + + override fun hashCode(): Int { + return originalBrowser?.hashCode() ?: super.hashCode() + } + + public fun resendState() { + if (state.html.isNotEmpty()) { + sendHtml(state.html) + state.executedJs.forEach { (code, url, line) -> + sendJs(code, url, line) + } + } + else if (state.externalUrl.isNotEmpty()) { + sendEvent(ServerBrowserEvent.LoadUrlEvent(id, state.externalUrl, false)) + } + else return + + sendMoveResizeEvent() + sendShowEvent(null) + sendSetOpenLinksInExternalBrowser(state.openInExternalBrowser) + } + + public fun setOpenLinksInExternalBrowser(openLinksInExternalBrowser: Boolean) { + state = state.copy(openInExternalBrowser = openLinksInExternalBrowser) + sendSetOpenLinksInExternalBrowser(openLinksInExternalBrowser) + } + + // Called only in headless + public fun onLifeSpanHandlerAdded(lifeSpanHandler: CefLifeSpanHandler) { + lifeSpanHandlers += lifeSpanHandler + lifeSpanHandler.onAfterCreated(this) // html loading starts only after browser creation + client.onAfterCreated(this) + } + + // Called only in headless + public fun onLifeSpanHandlerRemoved() { + lifeSpanHandlers.clear() + } + + public companion object { + + private val logger = Logger() + + private val ID_COUNTER = AtomicInteger() + + private val eventSender = EventSender.instance + + private val htmlMapField by lazy { + val clazz = Class.forName("com.intellij.ui.jcef.JBCefFileSchemeHandlerFactory") + + MethodHandles.privateLookupIn(clazz, MethodHandles.lookup()).findStaticVarHandle(clazz, "LOADHTML_REQUEST_MAP", Map::class.java) + } + + @Suppress("UNCHECKED_CAST") + private fun getHtmlMap(browser: CefBrowser): Map? = + (htmlMapField.get() as Map>)[browser] + + private fun sendEvent(event: ServerBrowserEvent) = eventSender.sendEvent(event) + + private val instances = Collections.synchronizedMap(mutableMapOf()) + + @Suppress("unused") // used in server + public fun getInstance(id: Int): ProjectorCefBrowser? = instances[id] + + public fun getClientInstances(client: CefClient): List = instances.values.filter { it.cefClient === client } + + internal fun updateAll() { + instances.values.forEach { + it.resendState() + } + } + + } +} diff --git a/projector-ij-common/src/main/kotlin/org/jetbrains/projector/ij/jcef/ProjectorCefFrame.kt b/projector-ij-common/src/main/kotlin/org/jetbrains/projector/ij/jcef/ProjectorCefFrame.kt new file mode 100644 index 000000000..95d220709 --- /dev/null +++ b/projector-ij-common/src/main/kotlin/org/jetbrains/projector/ij/jcef/ProjectorCefFrame.kt @@ -0,0 +1,92 @@ +/* + * MIT License + * + * Copyright (c) 2019-2022 JetBrains s.r.o. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jetbrains.projector.ij.jcef + +import org.cef.browser.CefFrame + +internal class ProjectorCefFrame : CefFrame { + override fun dispose() { + TODO("Not yet implemented") + } + + override fun getIdentifier(): Long { + TODO("Not yet implemented") + } + + override fun getURL(): String { + TODO("Not yet implemented") + } + + override fun getName(): String { + TODO("Not yet implemented") + } + + override fun isMain(): Boolean { + TODO("Not yet implemented") + } + + override fun isValid(): Boolean { + TODO("Not yet implemented") + } + + override fun isFocused(): Boolean { + TODO("Not yet implemented") + } + + override fun getParent(): CefFrame { + TODO("Not yet implemented") + } + + override fun executeJavaScript(p0: String?, p1: String?, p2: Int) { + TODO("Not yet implemented") + } + + override fun undo() { + TODO("Not yet implemented") + } + + override fun redo() { + TODO("Not yet implemented") + } + + override fun cut() { + TODO("Not yet implemented") + } + + override fun copy() { + TODO("Not yet implemented") + } + + override fun paste() { + TODO("Not yet implemented") + } + + override fun delete() { + TODO("Not yet implemented") + } + + override fun selectAll() { + TODO("Not yet implemented") + } +} diff --git a/projector-util-loading/build.gradle.kts b/projector-util-loading/build.gradle.kts index 3391e00e0..9bc019ecf 100644 --- a/projector-util-loading/build.gradle.kts +++ b/projector-util-loading/build.gradle.kts @@ -21,6 +21,8 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ +import com.intellij.openapi.util.BuildNumber + plugins { kotlin("jvm") `maven-publish` @@ -39,14 +41,21 @@ val coroutinesVersion: String by project val kotestVersion: String by project val intellijPlatformVersion: String by project +val intelliJVersionRemovedSuffix = intellijPlatformVersion.takeWhile { it.isDigit() || it == '.' } // in case of EAP +val intellijPlatformBuildNumber = BuildNumber.fromString(intelliJVersionRemovedSuffix)!! + dependencies { - compileOnly(project(":projector-common")) api(project(":projector-util-logging")) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") compileOnly("com.jetbrains.intellij.platform:bootstrap:$intellijPlatformVersion") - compileOnly("com.jetbrains.intellij.platform:util-base:$intellijPlatformVersion") + + if (intellijPlatformBuildNumber >= BuildNumber.fromString("213.6461.77")!!) { + compileOnly("com.jetbrains.intellij.platform:util-base:$intellijPlatformVersion") + } else { + compileOnly("com.jetbrains.intellij.platform:util-diagnostic:$intellijPlatformVersion") + } testImplementation("io.kotest:kotest-assertions-core:$kotestVersion") testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion")