diff options
author | Andrej Shadura <andrew.shadura@collabora.co.uk> | 2019-08-28 14:13:29 +0200 |
---|---|---|
committer | Andrej Shadura <andrew.shadura@collabora.co.uk> | 2019-08-29 17:48:13 +0200 |
commit | e19ef5983707e6a5c8d127f1ac8f02754cef82fd (patch) | |
tree | 9e3852cb9abc81ed6aa444465928d45fd7763dea /platform/script-debugger |
New upstream version 0~183.5153.4+dfsg
Diffstat (limited to 'platform/script-debugger')
164 files changed, 11713 insertions, 0 deletions
diff --git a/platform/script-debugger/backend/intellij.platform.scriptDebugger.backend.iml b/platform/script-debugger/backend/intellij.platform.scriptDebugger.backend.iml new file mode 100644 index 00000000..5ebae223 --- /dev/null +++ b/platform/script-debugger/backend/intellij.platform.scriptDebugger.backend.iml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" packagePrefix="org.jetbrains" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + <orderEntry type="module" module-name="intellij.platform.core" /> + <orderEntry type="module" module-name="intellij.platform.scriptDebugger.protocolReaderRuntime" /> + <orderEntry type="module" module-name="intellij.platform.ide" /> + <orderEntry type="module" module-name="intellij.platform.ide.impl" /> + <orderEntry type="library" name="netty-codec-http" level="project" /> + </component> +</module>
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/Breakpoint.kt b/platform/script-debugger/backend/src/debugger/Breakpoint.kt new file mode 100755 index 00000000..24de5d2b --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/Breakpoint.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +/** + * A breakpoint in the browser JavaScript virtual machine. The `set*` + * method invocations will not take effect until + * [.flush] is called. + */ +interface Breakpoint { + companion object { + /** + * This value is used when the corresponding parameter is absent + */ + const val EMPTY_VALUE: Int = -1 + + /** + * A breakpoint has this ID if it does not reflect an actual breakpoint in a + * JavaScript VM debugger. + */ + const val INVALID_ID: Int = -1 + } + + val target: BreakpointTarget + + val line: Int + + val column: Int + + /** + * @return whether this breakpoint is enabled + */ + /** + * Sets whether this breakpoint is enabled. + * Requires subsequent [.flush] call. + */ + var enabled: Boolean + + /** + * Sets the breakpoint condition as plain JavaScript (`null` to clear). + * Requires subsequent [.flush] call. + */ + var condition: String? + + val isResolved: Boolean + + /** + * Be aware! V8 doesn't provide reliable debugger API, so, sometimes actual locations is empty - in this case this methods return "true". + * V8 debugger doesn't report about resolved breakpoint if it is happened after initial breakpoint set. So, you cannot trust "actual locations". + */ + fun isActualLineCorrect(): Boolean = true +} + +/** + * Visitor interface that includes all extensions. + */ +interface TargetExtendedVisitor<R> : FunctionVisitor<R>, ScriptRegExpSupportVisitor<R> + + +/** + * Additional interface that user visitor may implement for [BreakpointTarget.accept] + * method. + */ +interface FunctionVisitor<R> : BreakpointTarget.Visitor<R> { + fun visitFunction(expression: String): R +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/BreakpointBase.kt b/platform/script-debugger/backend/src/debugger/BreakpointBase.kt new file mode 100644 index 00000000..cd995190 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/BreakpointBase.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.util.containers.ContainerUtil +import org.jetbrains.concurrency.Promise + +abstract class BreakpointBase<L : Any>(override val target: BreakpointTarget, + override var line: Int, + override val column: Int, + condition: String?, + enabled: Boolean) : Breakpoint { + val actualLocations: MutableList<L> = ContainerUtil.createLockFreeCopyOnWriteList<L>() + + /** + * Whether the breakpoint data have changed with respect + * to the JavaScript VM data + */ + @Volatile + protected var dirty: Boolean = false + + override val isResolved: Boolean + get() = !actualLocations.isEmpty() + + override var condition: String? = condition + set(value) { + if (field != value) { + field = value + dirty = true + } + } + + override var enabled: Boolean = enabled + set(value) { + if (value != field) { + field = value + dirty = true + } + } + + fun setActualLocations(value: List<L>?) { + actualLocations.clear() + if (!ContainerUtil.isEmpty(value)) { + actualLocations.addAll(value!!) + } + } + + fun setActualLocation(value: L?) { + actualLocations.clear() + if (value != null) { + actualLocations.add(value) + } + } + + abstract fun isVmRegistered(): Boolean + + override fun hashCode(): Int { + var result = line + result *= 31 + column + result *= 31 + (if (enabled) 1 else 0) + if (condition != null) { + result *= 31 + condition!!.hashCode() + } + result *= 31 + target.hashCode() + return result + } + + abstract fun flush(breakpointManager: BreakpointManager): Promise<*> +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/BreakpointManager.kt b/platform/script-debugger/backend/src/debugger/BreakpointManager.kt new file mode 100644 index 00000000..2f1e355e --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/BreakpointManager.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.util.Url +import org.jetbrains.concurrency.Promise +import java.util.* + +interface BreakpointManager { + enum class MUTE_MODE { + ALL, + ONE, + NONE + } + + val breakpoints: Iterable<Breakpoint> + + val regExpBreakpointSupported: Boolean + get() = false + + @Deprecated("use another overload") + fun setBreakpoint(target: BreakpointTarget, + line: Int, + condition: String? = null): Breakpoint { + throw UnsupportedOperationException() + } + + fun setBreakpoint(target: BreakpointTarget, + line: Int, + column: Int = Breakpoint.EMPTY_VALUE, + url: Url? = null, + condition: String? = null, + ignoreCount: Int = Breakpoint.EMPTY_VALUE): SetBreakpointResult + + fun remove(breakpoint: Breakpoint): Promise<*> + + /** + * Supports targets that refer to function text in form of function-returning + * JavaScript expression. + * E.g. you can set a breakpoint on the 5th line of user method addressed as + * 'PropertiesDialog.prototype.loadData'. + * Expression is calculated immediately and never recalculated again. + */ + val functionSupport: ((expression: String) -> BreakpointTarget)? + get() = null + + // Could be called multiple times for breakpoint + fun addBreakpointListener(listener: BreakpointListener) + + fun removeAll(): Promise<*> + + fun getMuteMode(): MUTE_MODE = BreakpointManager.MUTE_MODE.ONE + + /** + * Flushes the breakpoint parameter changes (set* methods) into the browser + * and invokes the callback once the operation has finished. This method must + * be called for the set* method invocations to take effect. + + */ + fun flush(breakpoint: Breakpoint): Promise<*> + + /** + * Asynchronously enables or disables all breakpoints on remote. 'Enabled' means that + * breakpoints behave as normal, 'disabled' means that VM doesn't stop on breakpoints. + * It doesn't update individual properties of [Breakpoint]s. Method call + * with a null value and not null callback simply returns current value. + */ + fun enableBreakpoints(enabled: Boolean): Promise<*> + + fun setBreakOnFirstStatement() + + fun isBreakOnFirstStatement(context: SuspendContext<*>): Boolean + + interface SetBreakpointResult + data class BreakpointExist(val existingBreakpoint: Breakpoint) : SetBreakpointResult + data class BreakpointCreated(val breakpoint: Breakpoint, val isResolved: Promise<out Breakpoint>) : SetBreakpointResult +} + +interface BreakpointListener : EventListener { + fun resolved(breakpoint: Breakpoint) + + fun errorOccurred(breakpoint: Breakpoint, errorMessage: String?) + + fun nonProvisionalBreakpointRemoved(breakpoint: Breakpoint) { + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/BreakpointManagerBase.kt b/platform/script-debugger/backend/src/debugger/BreakpointManagerBase.kt new file mode 100644 index 00000000..19f4749c --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/BreakpointManagerBase.kt @@ -0,0 +1,134 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger + +import com.intellij.concurrency.ConcurrentCollectionFactory +import com.intellij.openapi.util.text.StringUtil +import com.intellij.util.EventDispatcher +import com.intellij.util.SmartList +import com.intellij.util.Url +import com.intellij.util.containers.ContainerUtil +import gnu.trove.TObjectHashingStrategy +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.all +import org.jetbrains.concurrency.nullPromise +import org.jetbrains.concurrency.rejectedPromise +import java.util.concurrent.ConcurrentMap + +abstract class BreakpointManagerBase<T : BreakpointBase<*>> : BreakpointManager { + override val breakpoints: MutableSet<T> = ContainerUtil.newConcurrentSet<T>() + + protected val breakpointDuplicationByTarget: ConcurrentMap<T, T> = ConcurrentCollectionFactory.createMap<T, T>(object : TObjectHashingStrategy<T> { + override fun computeHashCode(b: T): Int { + var result = b.line + result *= 31 + b.column + if (b.condition != null) { + result *= 31 + b.condition!!.hashCode() + } + result *= 31 + b.target.hashCode() + return result + } + + override fun equals(b1: T, b2: T) = + b1.target.javaClass == b2.target.javaClass && + b1.target == b2.target && + b1.line == b2.line && + b1.column == b2.column && + StringUtil.equals(b1.condition, b2.condition) + }) + + protected val dispatcher: EventDispatcher<BreakpointListener> = EventDispatcher.create(BreakpointListener::class.java) + + protected abstract fun createBreakpoint(target: BreakpointTarget, line: Int, column: Int, condition: String?, ignoreCount: Int, enabled: Boolean): T + + protected abstract fun doSetBreakpoint(target: BreakpointTarget, url: Url?, breakpoint: T): Promise<out Breakpoint> + + override fun setBreakpoint(target: BreakpointTarget, + line: Int, + column: Int, + url: Url?, + condition: String?, + ignoreCount: Int): BreakpointManager.SetBreakpointResult { + val breakpoint = createBreakpoint(target, line, column, condition, ignoreCount, true) + val existingBreakpoint = breakpointDuplicationByTarget.putIfAbsent(breakpoint, breakpoint) + if (existingBreakpoint != null) { + return BreakpointManager.BreakpointExist(existingBreakpoint) + } + + breakpoints.add(breakpoint) + val promise = doSetBreakpoint(target, url, breakpoint) + .onError { dispatcher.multicaster.errorOccurred(breakpoint, it.message ?: it.toString()) } + return BreakpointManager.BreakpointCreated(breakpoint, promise) + } + + final override fun remove(breakpoint: Breakpoint): Promise<*> { + @Suppress("UNCHECKED_CAST") + val b = breakpoint as T + val existed = breakpoints.remove(b) + if (existed) { + breakpointDuplicationByTarget.remove(b) + } + return if (!existed || !b.isVmRegistered()) nullPromise() else doClearBreakpoint(b) + } + + final override fun removeAll(): Promise<*> { + val list = breakpoints.toList() + breakpoints.clear() + breakpointDuplicationByTarget.clear() + val promises = SmartList<Promise<*>>() + for (b in list) { + if (b.isVmRegistered()) { + promises.add(doClearBreakpoint(b)) + } + } + return promises.all() + } + + protected abstract fun doClearBreakpoint(breakpoint: T): Promise<*> + + final override fun addBreakpointListener(listener: BreakpointListener) { + dispatcher.addListener(listener) + } + + protected fun notifyBreakpointResolvedListener(breakpoint: T) { + if (breakpoint.isResolved) { + dispatcher.multicaster.resolved(breakpoint) + } + } + + @Suppress("UNCHECKED_CAST") + override fun flush(breakpoint: Breakpoint): Promise<*> = (breakpoint as T).flush(this) + + override fun enableBreakpoints(enabled: Boolean): Promise<*> = rejectedPromise<Any?>("Unsupported") + + override fun setBreakOnFirstStatement() { + } + + override fun isBreakOnFirstStatement(context: SuspendContext<*>): Boolean = false +} + +// used in goland +@Suppress("unused") +class DummyBreakpointManager : BreakpointManager { + override val breakpoints: Iterable<Breakpoint> + get() = emptyList() + + override fun setBreakpoint(target: BreakpointTarget, line: Int, column: Int, url: Url?, condition: String?, ignoreCount: Int): BreakpointManager.SetBreakpointResult { + throw UnsupportedOperationException() + } + + override fun remove(breakpoint: Breakpoint): Promise<*> = nullPromise() + + override fun addBreakpointListener(listener: BreakpointListener) { + } + + override fun removeAll(): Promise<*> = nullPromise() + + override fun flush(breakpoint: Breakpoint): Promise<*> = nullPromise() + + override fun enableBreakpoints(enabled: Boolean): Promise<*> = nullPromise() + + override fun setBreakOnFirstStatement() { + } + + override fun isBreakOnFirstStatement(context: SuspendContext<*>): Boolean = false +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/BreakpointTarget.java b/platform/script-debugger/backend/src/debugger/BreakpointTarget.java new file mode 100644 index 00000000..55b952b6 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/BreakpointTarget.java @@ -0,0 +1,128 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger; + +import org.jetbrains.annotations.NotNull; + +/** + * A reference to some JavaScript text that you can set breakpoints on. The reference may + * be in form of script name, script id etc. + * This type is essentially an Algebraic Type with several cases. Additional cases are provided + * in form of optional extensions. + * + * @see ScriptName + * @see ScriptId + */ +public abstract class BreakpointTarget { + /** + * Dispatches call on the actual Target type. + * + * @param visitor user-provided {@link Visitor} that may also implement some additional + * interfaces (for extended types) that is checked on runtime + */ + public abstract <R> R accept(Visitor<R> visitor); + + public interface Visitor<R> { + R visitScriptName(String scriptName); + + R visitScript(Script script); + + R visitUnknown(BreakpointTarget target); + } + + /** + * A target that refers to a script by its id + */ + public static final class ScriptId extends BreakpointTarget { + public final Script script; + + public ScriptId(@NotNull Script script) { + this.script = script; + } + + @Override + public <R> R accept(Visitor<R> visitor) { + return visitor.visitScript(script); + } + + @Override + public String toString() { + return script.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + return script.equals(((ScriptId)o).script); + } + + @Override + public int hashCode() { + return script.hashCode(); + } + } + + public abstract String toString(); + + /** + * A target that refers to a script by its name. Breakpoint will be set on every matching script currently loaded in VM. + */ + public static final class ScriptName extends BreakpointTarget { + private final String name; + + public ScriptName(@NotNull String name) { + this.name = name; + } + + @NotNull + public String getName() { + return name; + } + + @Override + public String toString() { + return name; + } + + @Override + public <R> R accept(@NotNull Visitor<R> visitor) { + return visitor.visitScriptName(name); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + return name.equals(((ScriptName)o).name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/CallFrame.kt b/platform/script-debugger/backend/src/debugger/CallFrame.kt new file mode 100755 index 00000000..3fde012a --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/CallFrame.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import org.jetbrains.concurrency.Promise + +interface CallFrame { + /** + * @return the scopes known in this frame + */ + val variableScopes: List<Scope> + + val hasOnlyGlobalScope: Boolean + + /** + * receiver variable known in this frame ("this" variable) + * Computed variable must be null if no receiver variable + */ + val receiverVariable: Promise<Variable?> + + val line: Int + + val column: Int + + /** + * @return the name of the current function of this frame + */ + val functionName: String? + + /** + * @return context for evaluating expressions in scope of this frame + */ + val evaluateContext: EvaluateContext + + /** + * @see com.intellij.xdebugger.frame.XStackFrame.getEqualityObject + */ + val equalityObject: Any + + /** + * Name of function which scheduled some handler for top frames of async stack. + */ + val asyncFunctionName: String? + + val isFromAsyncStack: Boolean +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/CallFrameBase.kt b/platform/script-debugger/backend/src/debugger/CallFrameBase.kt new file mode 100644 index 00000000..46d73486 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/CallFrameBase.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.openapi.util.NotNullLazyValue + +const val RECEIVER_NAME: String = "this" + +@Deprecated("") +/** + * Use kotlin - base class is not required in this case (no boilerplate code) + */ +/** + * You must initialize [.scopes] or override [.getVariableScopes] + */ +abstract class CallFrameBase(override val functionName: String?, override val line: Int, override val column: Int, override val evaluateContext: EvaluateContext) : CallFrame { + protected var scopes: NotNullLazyValue<List<Scope>>? = null + + override var hasOnlyGlobalScope: Boolean = false + protected set(value: Boolean) { + field = value + } + + override val variableScopes: List<Scope> + get() = scopes!!.value + + override val asyncFunctionName: String? = null + + override val isFromAsyncStack: Boolean = false +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/DebugEventListener.java b/platform/script-debugger/backend/src/debugger/DebugEventListener.java new file mode 100755 index 00000000..6a3a5a65 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/DebugEventListener.java @@ -0,0 +1,74 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.EventListener; + +public interface DebugEventListener extends EventListener { + /** + * Reports the virtual machine has suspended (on hitting + * breakpoints or a step end). The {@code context} can be used to access the + * current backtrace. + */ + default void suspended(@NotNull SuspendContext<?> context) { + } + + /** + * Reports the virtual machine has resumed. This can happen + * asynchronously, due to a user action in the browser (without explicitly resuming the VM through + * @param vm + */ + default void resumed(@NotNull Vm vm) { + } + + /** + * Reports that a new script has been loaded. + */ + default void scriptAdded(@NotNull Vm vm, @NotNull Script script, @Nullable String sourceMapUrl) { + } + + /** + * Reports that the script has been collected and is no longer used in VM. + */ + default void scriptRemoved(@NotNull Script script) { + } + + default void scriptsCleared() { + } + + /** + * Reports that script source has been altered in remote VM. + */ + default void scriptContentChanged(@NotNull Script newScript) { + } + + /** + * Reports a navigation event on the target. + * + * @param newUrl the new URL of the debugged target + */ + default void navigated(String newUrl) { + } + + default void errorOccurred(@NotNull String errorMessage) { + } + + default void childVmAdded(@NotNull Vm childVm) { + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/DeclarativeScope.kt b/platform/script-debugger/backend/src/debugger/DeclarativeScope.kt new file mode 100644 index 00000000..b413c249 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/DeclarativeScope.kt @@ -0,0 +1,22 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger + +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.cancelledPromise +import org.jetbrains.debugger.values.ObjectValue +import org.jetbrains.debugger.values.ValueManager + +abstract class DeclarativeScope<VALUE_MANAGER : ValueManager>(type: ScopeType, description: String? = null) : ScopeBase(type, description) { + protected abstract val childrenManager: VariablesHost<VALUE_MANAGER> + + override val variablesHost: VariablesHost<*> + get() = childrenManager + + protected fun loadScopeObjectProperties(value: ObjectValue): Promise<List<Variable>> { + if (childrenManager.valueManager.isObsolete) { + return cancelledPromise() + } + + return value.properties.onSuccess { childrenManager.updateCacheStamp() } + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/EvaluateContext.kt b/platform/script-debugger/backend/src/debugger/EvaluateContext.kt new file mode 100644 index 00000000..3c82383a --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/EvaluateContext.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.openapi.project.Project +import org.jetbrains.concurrency.Promise +import org.jetbrains.debugger.values.Value + +data class EvaluateResult(val value: Value, val wasThrown: Boolean = false) + +/** + * A context in which watch expressions may be evaluated. Typically corresponds to stack frame + * of suspended process, but may also be detached from any stack frame + */ +interface EvaluateContext { + /** + * Evaluates an arbitrary `expression` in the particular context. + * Previously loaded [org.jetbrains.debugger.values.ObjectValue]s can be addressed from the expression if listed in + * additionalContext parameter. + */ + fun evaluate(expression: String, additionalContext: Map<String, Any>? = null, enableBreak: Boolean = false, project: Project? = null): Promise<EvaluateResult> + + /** + * optional to implement, some protocols, WIP for example, require you to release remote objects + */ + fun withValueManager(objectGroup: String): EvaluateContext + + /** + * If you evaluate "foo.bar = 4" and want to update Variables view (and all other clients), you can use use this task + * @param promise + */ + fun refreshOnDone(promise: Promise<*>): Promise<*> + + /** + * call only if withLoader was called before + */ + fun releaseObjects() { + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/EvaluateContextBase.kt b/platform/script-debugger/backend/src/debugger/EvaluateContextBase.kt new file mode 100644 index 00000000..cc622e9a --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/EvaluateContextBase.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.thenRun +import org.jetbrains.debugger.values.ValueManager + +abstract class EvaluateContextBase<VALUE_MANAGER : ValueManager>(val valueManager: VALUE_MANAGER) : EvaluateContext { + override fun withValueManager(objectGroup: String): EvaluateContextBase<VALUE_MANAGER> = this + + override fun refreshOnDone(promise: Promise<*>): Promise<Unit> = promise.thenRun { valueManager.clearCaches() } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/ExceptionCatchMode.java b/platform/script-debugger/backend/src/debugger/ExceptionCatchMode.java new file mode 100644 index 00000000..1b757939 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ExceptionCatchMode.java @@ -0,0 +1,36 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger; + +/** + * Defines when VM will break on exception throw (before stack unwind happened) + */ +public enum ExceptionCatchMode { + /** + * VM always breaks when exception is being thrown + */ + ALL, + + /** + * VM breaks when exception is being thrown without try-catch that is going to catch it + */ + UNCAUGHT, + + /** + * VM doesn't break when exception is being thrown + */ + NONE +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/ExceptionData.java b/platform/script-debugger/backend/src/debugger/ExceptionData.java new file mode 100755 index 00000000..f423b5a9 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ExceptionData.java @@ -0,0 +1,45 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger; + +import com.intellij.util.ThreeState; +import org.jetbrains.debugger.values.Value; + +/** + * A JavaScript exception data holder for exceptions reported by a JavaScript + * virtual machine. + */ +public interface ExceptionData { + /** + * @return the thrown exception value + */ + Value getExceptionValue(); + + /** + * @return whether this exception is uncaught + */ + ThreeState isUncaught(); + + /** + * @return the text of the source line where the exception was thrown or null + */ + String getSourceText(); + + /** + * @return the exception description (plain text) + */ + String getExceptionMessage(); +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/ExceptionDataBase.java b/platform/script-debugger/backend/src/debugger/ExceptionDataBase.java new file mode 100644 index 00000000..2dd0f5d7 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ExceptionDataBase.java @@ -0,0 +1,31 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger; + +import org.jetbrains.debugger.values.Value; + +public abstract class ExceptionDataBase implements ExceptionData { + private final Value exceptionValue; + + protected ExceptionDataBase(Value exceptionValue) { + this.exceptionValue = exceptionValue; + } + + @Override + public final Value getExceptionValue() { + return exceptionValue; + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/ObjectProperty.kt b/platform/script-debugger/backend/src/debugger/ObjectProperty.kt new file mode 100644 index 00000000..8162f245 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ObjectProperty.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import org.jetbrains.debugger.values.FunctionValue + +/** + * Exposes additional data if variable is a property of object and its property descriptor + * is available. + */ +interface ObjectProperty : Variable { + val isWritable: Boolean + + val getter: FunctionValue? + + val setter: FunctionValue? + + + val isConfigurable: Boolean + + val isEnumerable: Boolean +} diff --git a/platform/script-debugger/backend/src/debugger/ObjectPropertyImpl.kt b/platform/script-debugger/backend/src/debugger/ObjectPropertyImpl.kt new file mode 100644 index 00000000..2fd3f0aa --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ObjectPropertyImpl.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.util.BitUtil +import org.jetbrains.debugger.values.FunctionValue +import org.jetbrains.debugger.values.Value + +class ObjectPropertyImpl(name: String, + value: Value?, + override val getter: FunctionValue? = null, + override val setter: FunctionValue? = null, + valueModifier: ValueModifier? = null, + private val flags: Int = 0) : VariableImpl(name, value, valueModifier), ObjectProperty { + companion object { + val WRITABLE: Int = 0x01 + val CONFIGURABLE: Int = 0x02 + val ENUMERABLE: Int = 0x04 + } + + override val isWritable: Boolean + get() = BitUtil.isSet(flags, WRITABLE) + + override val isConfigurable: Boolean + get() = BitUtil.isSet(flags, CONFIGURABLE) + + override val isEnumerable: Boolean + get() = BitUtil.isSet(flags, ENUMERABLE) +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/Scope.kt b/platform/script-debugger/backend/src/debugger/Scope.kt new file mode 100644 index 00000000..36e145dc --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/Scope.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import org.jetbrains.debugger.values.ObjectValue + +enum class ScopeType { + GLOBAL, + LOCAL, + WITH, + CLOSURE, + CATCH, + LIBRARY, + CLASS, + INSTANCE, + BLOCK, + SCRIPT, + UNKNOWN +} + +interface Scope { + val type: ScopeType + + /** + * Class or function or file name + */ + val description: String? + + val variablesHost: VariablesHost<*> + + val isGlobal: Boolean +} + +abstract class ScopeBase(override val type: ScopeType, override val description: String?) : Scope { + override val isGlobal: Boolean + get() = type === ScopeType.GLOBAL || type === ScopeType.LIBRARY +} + +class ObjectScope(type: ScopeType, private val value: ObjectValue) : ScopeBase(type, value.valueString), Scope { + override val variablesHost: VariablesHost<*> + get() = value.variablesHost +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/Script.kt b/platform/script-debugger/backend/src/debugger/Script.kt new file mode 100755 index 00000000..e2d57e08 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/Script.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.openapi.util.UserDataHolderEx +import com.intellij.util.Url +import org.jetbrains.debugger.sourcemap.SourceMap + +interface Script : UserDataHolderEx { + enum class Type { + /** A native, internal JavaScript VM script */ + NATIVE, + + /** A script supplied by an extension */ + EXTENSION, + + /** A normal user script */ + NORMAL + } + + val type: Type + + var sourceMap: SourceMap? + + val url: Url + + val functionName: String? + get() = null + + val line: Int + + val column: Int + + val endLine: Int + + val isWorker: Boolean +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/ScriptBase.kt b/platform/script-debugger/backend/src/debugger/ScriptBase.kt new file mode 100755 index 00000000..0f41a9cf --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ScriptBase.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.openapi.util.UserDataHolderBase +import com.intellij.util.Url +import org.jetbrains.concurrency.Promise +import org.jetbrains.debugger.sourcemap.SourceMap + +abstract class ScriptBase(override val type: Script.Type, + override val url: Url, + line: Int, + override val column: Int, + override val endLine: Int) : UserDataHolderBase(), Script { + override val line: Int = Math.max(line, 0) + + @SuppressWarnings("UnusedDeclaration") + @Volatile + private var source: Promise<String>? = null + + override var sourceMap: SourceMap? = null + + override fun toString(): String = "[url=$url, lineRange=[$line;$endLine]]" + + override val isWorker: Boolean = false +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/ScriptManager.kt b/platform/script-debugger/backend/src/debugger/ScriptManager.kt new file mode 100644 index 00000000..a8cf29a6 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ScriptManager.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.util.Processor +import com.intellij.util.Url +import org.jetbrains.concurrency.Promise +import org.jetbrains.debugger.values.FunctionValue + +const val VM_SCHEME: String = "vm" + +interface ScriptManager { + fun getSource(script: Script): Promise<String> + + fun hasSource(script: Script): Boolean + + fun containsScript(script: Script): Boolean + + fun forEachScript(scriptProcessor: (Script) -> Boolean) + + fun forEachScript(scriptProcessor: Processor<Script>): Unit = forEachScript { scriptProcessor.process(it)} + + fun getScript(function: FunctionValue): Promise<Script> + + fun getScript(frame: CallFrame): Script? + + fun findScriptByUrl(rawUrl: String): Script? + + fun findScriptByUrl(url: Url): Script? + + fun findScriptById(id: String): Script? = null +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/ScriptManagerBase.kt b/platform/script-debugger/backend/src/debugger/ScriptManagerBase.kt new file mode 100644 index 00000000..d863f7bf --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ScriptManagerBase.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.util.Url +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.PromiseManager +import org.jetbrains.concurrency.rejectedPromise + +abstract class ScriptManagerBase<SCRIPT : ScriptBase> : ScriptManager { + @Suppress("UNCHECKED_CAST") + @SuppressWarnings("unchecked") + private val scriptSourceLoader = object : PromiseManager<ScriptBase, String>(ScriptBase::class.java) { + override fun load(script: ScriptBase) = loadScriptSource(script as SCRIPT) + } + + protected abstract fun loadScriptSource(script: SCRIPT): Promise<String> + + override fun getSource(script: Script): Promise<String> { + if (!containsScript(script)) { + return rejectedPromise("No Script") + } + @Suppress("UNCHECKED_CAST") + return scriptSourceLoader.get(script as SCRIPT) + } + + override fun hasSource(script: Script): Boolean { + @Suppress("UNCHECKED_CAST") + return scriptSourceLoader.has(script as SCRIPT) + } + + fun setSource(script: SCRIPT, source: String?) { + scriptSourceLoader.set(script, source) + } +} + +val Url.isSpecial: Boolean + get() = !isInLocalFileSystem && (scheme == null || scheme == VM_SCHEME || authority == null)
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/ScriptManagerBaseEx.kt b/platform/script-debugger/backend/src/debugger/ScriptManagerBaseEx.kt new file mode 100644 index 00000000..a20ef260 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ScriptManagerBaseEx.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.util.Url +import com.intellij.util.Urls +import com.intellij.util.containers.ContainerUtil +import java.util.concurrent.ConcurrentMap + +abstract class ScriptManagerBaseEx<SCRIPT : ScriptBase> : ScriptManagerBase<SCRIPT>() { + protected val idToScript: ConcurrentMap<String, SCRIPT> = ContainerUtil.newConcurrentMap<String, SCRIPT>() + + final override fun forEachScript(scriptProcessor: (Script) -> Boolean) { + for (script in idToScript.values) { + if (!scriptProcessor(script)) { + return + } + } + } + + final override fun findScriptById(id: String): SCRIPT? = idToScript[id] + + fun clear(listener: DebugEventListener) { + idToScript.clear() + listener.scriptsCleared() + } + + final override fun findScriptByUrl(rawUrl: String): SCRIPT? = findScriptByUrl(rawUrlToOurUrl(rawUrl)) + + final override fun findScriptByUrl(url: Url): SCRIPT? { + return idToScript.values.find { url == it.url } + // TODO Searching ignoring parameters may be fragile, because parameters define script e.g. in webpack. Consider dropping it. + ?: idToScript.values.find { url.equalsIgnoreParameters(it.url) } + } + + open fun rawUrlToOurUrl(rawUrl: String): Url = Urls.parseEncoded(rawUrl)!! +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/ScriptRegExpBreakpointTarget.kt b/platform/script-debugger/backend/src/debugger/ScriptRegExpBreakpointTarget.kt new file mode 100644 index 00000000..bfd9d98e --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ScriptRegExpBreakpointTarget.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +class ScriptRegExpBreakpointTarget(private val regExp: String, val language: String? = null) : BreakpointTarget() { + override fun <R> accept(visitor: BreakpointTarget.Visitor<R>): R { + if (visitor is ScriptRegExpSupportVisitor<*>) { + return (visitor as ScriptRegExpSupportVisitor<R>).visitRegExp(this) + } + else { + return visitor.visitUnknown(this) + } + } + + override fun toString(): String = regExp + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + if (other == null || javaClass != other.javaClass) { + return false + } + return regExp == (other as ScriptRegExpBreakpointTarget).regExp + } + + override fun hashCode(): Int = regExp.hashCode() +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/ScriptRegExpSupportVisitor.java b/platform/script-debugger/backend/src/debugger/ScriptRegExpSupportVisitor.java new file mode 100644 index 00000000..9ab738d0 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ScriptRegExpSupportVisitor.java @@ -0,0 +1,26 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger; + +import org.jetbrains.annotations.NotNull; + +/** + * Additional interface that user visitor may implement for {@link BreakpointTarget#accept} + * method. + */ +public interface ScriptRegExpSupportVisitor<R> extends BreakpointTarget.Visitor<R> { + R visitRegExp(@NotNull ScriptRegExpBreakpointTarget target); +} diff --git a/platform/script-debugger/backend/src/debugger/StandaloneVmHelper.kt b/platform/script-debugger/backend/src/debugger/StandaloneVmHelper.kt new file mode 100644 index 00000000..1d7889ef --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/StandaloneVmHelper.kt @@ -0,0 +1,84 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger + +import com.intellij.util.io.addChannelListener +import com.intellij.util.io.shutdownIfOio +import io.netty.channel.Channel +import org.jetbrains.concurrency.AsyncPromise +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.errorIfNotMessage +import org.jetbrains.concurrency.nullPromise +import org.jetbrains.jsonProtocol.Request +import org.jetbrains.rpc.CONNECTION_CLOSED_MESSAGE +import org.jetbrains.rpc.LOG +import org.jetbrains.rpc.MessageProcessor + +open class StandaloneVmHelper(private val vm: Vm, private val messageProcessor: MessageProcessor, channel: Channel) : AttachStateManager { + @Volatile + private var channel: Channel? = channel + + fun getChannelIfActive(): Channel? { + val currentChannel = channel + return if (currentChannel == null || !currentChannel.isActive) null else currentChannel + } + + fun write(content: Any): Boolean { + val channel = getChannelIfActive() + return channel != null && !channel.writeAndFlush(content).isCancelled + } + + interface VmEx : Vm { + fun createDisconnectRequest(): Request<out Any>? + } + + override val isAttached: Boolean + get() = channel != null + + override fun detach(): Promise<*> { + val currentChannel = channel ?: return nullPromise() + + messageProcessor.cancelWaitingRequests() + val disconnectRequest = (vm as? VmEx)?.createDisconnectRequest() + val promise = AsyncPromise<Any?>() + if (disconnectRequest == null) { + messageProcessor.closed() + channel = null + } + else { + messageProcessor.send(disconnectRequest) + .onError { + if (it.message != CONNECTION_CLOSED_MESSAGE) { + LOG.errorIfNotMessage(it) + } + } + // we don't wait response because 1) no response to "disconnect" message (V8 for example) 2) closed message manager just ignore any incoming messages + currentChannel.flush() + messageProcessor.closed() + channel = null + messageProcessor.cancelWaitingRequests() + } + closeChannel(currentChannel, promise) + return promise + } + + protected open fun closeChannel(channel: Channel, promise: AsyncPromise<Any?>) { + doCloseChannel(channel, promise) + } +} + +fun doCloseChannel(channel: Channel, promise: AsyncPromise<Any?>) { + channel.close().addChannelListener { + try { + it.channel().eventLoop().shutdownIfOio() + } + finally { + val error = it.cause() + if (error == null) { + promise.setResult(null) + } + else { + promise.setError(error) + } + } + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/SuspendContext.kt b/platform/script-debugger/backend/src/debugger/SuspendContext.kt new file mode 100755 index 00000000..2b280e2e --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/SuspendContext.kt @@ -0,0 +1,66 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger + +import org.jetbrains.concurrency.Promise +import org.jetbrains.debugger.values.ValueManager + +/** + * An object that matches the execution state of the VM while suspended + */ +interface SuspendContext<out CALL_FRAME : CallFrame> { + + val script: Script? + get() = topFrame?.let { vm.scriptManager.getScript(it) } + + /** + * @return the current exception state if execution was paused because of exception, or `null` otherwise. + */ + val exceptionData: ExceptionData? + get() = null + + val topFrame: CALL_FRAME? + + /** + * Call frames for the current suspended state (from the innermost (top) frame to the main (bottom) frame) + */ + val frames: Promise<Array<CallFrame>> + + /** + * list of the breakpoints hit on VM suspension with which this + * context is associated. An empty collection if the suspension was + * not related to hitting breakpoints (e.g. a step end) + */ + val breakpointsHit: List<Breakpoint> + + val hasUnresolvedBreakpointsHit: Boolean + get() = false + + val valueManager: ValueManager + + val vm: Vm + get() = throw UnsupportedOperationException() +} + +abstract class ContextDependentAsyncResultConsumer<T>(private val context: SuspendContext<*>) : java.util.function.Consumer<T> { + final override fun accept(result: T) { + val vm = context.vm + if (vm.attachStateManager.isAttached && !vm.suspendContextManager.isContextObsolete(context)) { + accept(result, vm) + } + } + + protected abstract fun accept(result: T, vm: Vm) +} + + +inline fun <T> Promise<T>.onSuccess(context: SuspendContext<*>, crossinline handler: (result: T) -> Unit): Promise<T> { + return onSuccess(object : ContextDependentAsyncResultConsumer<T>(context) { + override fun accept(result: T, vm: Vm) = handler(result) + }) +} + +inline fun Promise<*>.onError(context: SuspendContext<*>, crossinline handler: (error: Throwable) -> Unit): Promise<out Any> { + return onError(object : ContextDependentAsyncResultConsumer<Throwable>(context) { + override fun accept(result: Throwable, vm: Vm) = handler(result) + }) +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/SuspendContextBase.kt b/platform/script-debugger/backend/src/debugger/SuspendContextBase.kt new file mode 100644 index 00000000..5e7018a5 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/SuspendContextBase.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +abstract class SuspendContextBase<F : CallFrame> : SuspendContext<F> { +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/SuspendContextManager.kt b/platform/script-debugger/backend/src/debugger/SuspendContextManager.kt new file mode 100644 index 00000000..275ba088 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/SuspendContextManager.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import org.jetbrains.concurrency.Promise + +interface SuspendContextManager<CALL_FRAME : CallFrame> { + /** + * Tries to suspend VM. If successful, [DebugEventListener.suspended] will be called. + */ + fun suspend(): Promise<*> + + val context: SuspendContext<CALL_FRAME>? + + val contextOrFail: SuspendContext<CALL_FRAME> + + fun isContextObsolete(context: SuspendContext<*>): Boolean = this.context !== context + + fun setOverlayMessage(message: String?) + + /** + * Resumes the VM execution. This context becomes invalid until another context is supplied through the + * [DebugEventListener.suspended] event. + * @param stepAction to perform + * * + * @param stepCount steps to perform (not used if `stepAction == CONTINUE`) + */ + fun continueVm(stepAction: StepAction, stepCount: Int = 1): Promise<*> + + val isRestartFrameSupported: Boolean + + /** + * Restarts a frame (all frames above are dropped from the stack, this frame is started over). + * for success the boolean parameter + * is true if VM has been resumed and is expected to get suspended again in a moment (with + * a standard 'resumed' notification), and is false if call frames list is already updated + * without VM state change (this case presently is never actually happening) + */ + fun restartFrame(callFrame: CALL_FRAME): Promise<Boolean> + + /** + * @return whether reset operation is supported for the particular callFrame + */ + fun canRestartFrame(callFrame: CallFrame): Boolean +} + +enum class StepAction { + /** + * Resume the JavaScript execution. + */ + CONTINUE, + + /** + * Step into the current statement. + */ + IN, + + /** + * Step over the current statement. + */ + OVER, + + /** + * Step out of the current function. + */ + OUT +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/SuspendContextManagerBase.kt b/platform/script-debugger/backend/src/debugger/SuspendContextManagerBase.kt new file mode 100644 index 00000000..3b3b3dea --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/SuspendContextManagerBase.kt @@ -0,0 +1,64 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger + +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.rejectedPromise +import org.jetbrains.concurrency.resolvedPromise +import java.util.concurrent.atomic.AtomicReference + +abstract class SuspendContextManagerBase<T : SuspendContext<CALL_FRAME>, CALL_FRAME : CallFrame> : SuspendContextManager<CALL_FRAME> { + val contextRef: AtomicReference<T> = AtomicReference() + + protected abstract val debugListener: DebugEventListener + + fun setContext(newContext: T) { + if (!contextRef.compareAndSet(null, newContext)) { + throw IllegalStateException("Attempt to set context, but current suspend context is already exists") + } + } + + open fun updateContext(newContext: SuspendContext<*>) { + } + + // dismiss context on resumed + protected fun dismissContext() { + contextRef.get()?.let { + contextDismissed(it) + } + } + + protected fun dismissContextOnDone(promise: Promise<*>): Promise<*> { + val context = contextOrFail + promise.onSuccess { contextDismissed(context) } + return promise + } + + fun contextDismissed(context: T) { + if (!contextRef.compareAndSet(context, null)) { + throw IllegalStateException("Expected $context, but another suspend context exists") + } + context.valueManager.markObsolete() + debugListener.resumed(context.vm) + } + + override val context: SuspendContext<CALL_FRAME>? + get() = contextRef.get() + + override val contextOrFail: T + get() = contextRef.get() ?: throw IllegalStateException("No current suspend context") + + override fun suspend(): Promise<out Any?> = if (context == null) doSuspend() else resolvedPromise() + + protected abstract fun doSuspend(): Promise<*> + + override fun setOverlayMessage(message: String?) { + } + + override fun restartFrame(callFrame: CALL_FRAME): Promise<Boolean> = restartFrame(callFrame, contextOrFail) + + protected open fun restartFrame(callFrame: CALL_FRAME, currentContext: T): Promise<Boolean> = rejectedPromise<Boolean>("Unsupported") + + override fun canRestartFrame(callFrame: CallFrame): Boolean = false + + override val isRestartFrameSupported: Boolean = false +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/ValueModifier.kt b/platform/script-debugger/backend/src/debugger/ValueModifier.kt new file mode 100644 index 00000000..4691fc81 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ValueModifier.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import org.jetbrains.concurrency.Promise +import org.jetbrains.debugger.values.Value + +interface ValueModifier { + // expression can contains reference to another variables in current scope, so, we should evaluate it before set + // https://youtrack.jetbrains.com/issue/WEB-2342#comment=27-512122 + + // we don't worry about performance in case of simple primitive values - boolean/string/numbers, + // it works quickly and we don't want to complicate our code and debugger SDK + fun setValue(variable: Variable, newValue: String, evaluateContext: EvaluateContext): Promise<*> + + fun setValue(variable: Variable, newValue: Value, evaluateContext: EvaluateContext): Promise<*> + + fun evaluateGet(variable: Variable, evaluateContext: EvaluateContext): Promise<Value> +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/ValueModifierUtil.kt b/platform/script-debugger/backend/src/debugger/ValueModifierUtil.kt new file mode 100644 index 00000000..bdd75c63 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/ValueModifierUtil.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.thenAsyncAccept +import org.jetbrains.debugger.values.Value +import org.jetbrains.io.JsonUtil +import java.util.* +import java.util.regex.Pattern + +private val KEY_NOTATION_PROPERTY_NAME_PATTERN = Pattern.compile("[\\p{L}_$]+[\\d\\p{L}_$]*") + +object ValueModifierUtil { + fun setValue(variable: Variable, + newValue: String, + evaluateContext: EvaluateContext, + modifier: ValueModifier): Promise<Any?> = evaluateContext.evaluate(newValue) + .thenAsyncAccept { modifier.setValue(variable, it.value, evaluateContext) } + + fun evaluateGet(variable: Variable, + host: Any, + evaluateContext: EvaluateContext, + selfName: String): Promise<Value> { + val builder = StringBuilder(selfName) + appendUnquotedName(builder, variable.name) + return evaluateContext.evaluate(builder.toString(), Collections.singletonMap(selfName, host), false) + .then { + variable.value = it.value + it.value + } + } + + fun propertyNamesToString(list: List<String>, quotedAware: Boolean): String { + val builder = StringBuilder() + for (i in list.indices.reversed()) { + val name = list[i] + doAppendName(builder, name, quotedAware && (name[0] == '"' || name[0] == '\'')) + } + return builder.toString() + } + + fun appendUnquotedName(builder: StringBuilder, name: String) { + doAppendName(builder, name, false) + } +} + +private fun doAppendName(builder: StringBuilder, name: String, quoted: Boolean) { + val isProperty = !builder.isEmpty() + if (isProperty) { + val useKeyNotation = !quoted && KEY_NOTATION_PROPERTY_NAME_PATTERN.matcher(name).matches() + if (useKeyNotation) { + builder.append('.').append(name) + } + else { + builder.append('[') + if (quoted) builder.append(name) + else JsonUtil.escape(name, builder) + builder.append(']') + } + } + else { + builder.append(name) + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/Variable.java b/platform/script-debugger/backend/src/debugger/Variable.java new file mode 100755 index 00000000..98cfe3e9 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/Variable.java @@ -0,0 +1,49 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.debugger.values.Value; + +public interface Variable { + /** + * @return whether it is possible to read this variable + */ + boolean isReadable(); + + /** + * Returns the value of this variable. + * + * @return a Value corresponding to this variable. {@code null} if the property has accessor descriptor + * @see #isReadable() + */ + @Nullable + Value getValue(); + + void setValue(Value value); + + @NotNull + String getName(); + + /** + * @return whether it is possible to modify this variable + */ + boolean isMutable(); + + @Nullable + ValueModifier getValueModifier(); +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/VariableImpl.java b/platform/script-debugger/backend/src/debugger/VariableImpl.java new file mode 100644 index 00000000..178420c3 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/VariableImpl.java @@ -0,0 +1,75 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.debugger.values.Value; + +public class VariableImpl implements Variable { + protected volatile Value value; + private final String name; + + private final ValueModifier valueModifier; + + public VariableImpl(@NotNull String name, @Nullable Value value, @Nullable ValueModifier valueModifier) { + this.name = name; + this.value = value; + this.valueModifier = valueModifier; + } + + public VariableImpl(@NotNull String name, @NotNull Value value) { + this(name, value, null); + } + + @Nullable + @Override + public final ValueModifier getValueModifier() { + return valueModifier; + } + + @NotNull + @Override + public final String getName() { + return name; + } + + @Nullable + @Override + public final Value getValue() { + return value; + } + + @Override + public void setValue(Value value) { + this.value = value; + } + + @Override + public boolean isMutable() { + return valueModifier != null; + } + + @Override + public boolean isReadable() { + return true; + } + + @Override + public String toString() { + return "[Variable: name=" + getName() + ", value=" + getValue() + ']'; + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/VariablesHost.java b/platform/script-debugger/backend/src/debugger/VariablesHost.java new file mode 100644 index 00000000..ba9823b6 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/VariablesHost.java @@ -0,0 +1,88 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.concurrency.Promise; +import org.jetbrains.concurrency.PromiseManager; +import org.jetbrains.concurrency.Promises; +import org.jetbrains.debugger.values.ValueManager; + +import java.util.List; + +public abstract class VariablesHost<VALUE_MANAGER extends ValueManager> { + @SuppressWarnings("unchecked") + private static final PromiseManager<VariablesHost, List<Variable>> VARIABLES_LOADER = + new PromiseManager<VariablesHost, List<Variable>>(VariablesHost.class) { + @Override + public boolean isUpToDate(@NotNull VariablesHost host, @NotNull List<Variable> data) { + return host.valueManager.getCacheStamp() == host.cacheStamp; + } + + @NotNull + @Override + public Promise load(@NotNull VariablesHost host) { + return host.valueManager.isObsolete() ? Promises.cancelledPromise() : host.load(); + } + }; + + @SuppressWarnings("UnusedDeclaration") + private volatile Promise<List<Variable>> result; + + private volatile int cacheStamp = -1; + + public final VALUE_MANAGER valueManager; + + public VariablesHost(@NotNull VALUE_MANAGER manager) { + valueManager = manager; + } + + /** + * You must call {@link #updateCacheStamp()} when data loaded + */ + @NotNull + public final Promise<List<Variable>> get() { + return VARIABLES_LOADER.get(this); + } + + @Nullable + public final Promise.State getState() { + return VARIABLES_LOADER.getState(this); + } + + public final void set(@NotNull List<Variable> result) { + updateCacheStamp(); + VARIABLES_LOADER.set(this, result); + } + + @NotNull + protected abstract Promise<List<Variable>> load(); + + public final void updateCacheStamp() { + cacheStamp = valueManager.getCacheStamp(); + } + + /** + * Some backends requires to reload the whole call stack on scope variable modification, but not all API is asynchronous (compromise, to not increase complexity), + * for example, {@link CallFrame#getVariableScopes()} is not asynchronous method. So, you must use returned callback to postpone your code working with updated data. + */ + public Promise<?> clearCaches() { + cacheStamp = -1; + VARIABLES_LOADER.reset(this); + return Promises.resolvedPromise(); + } +} diff --git a/platform/script-debugger/backend/src/debugger/Vm.kt b/platform/script-debugger/backend/src/debugger/Vm.kt new file mode 100644 index 00000000..b7ef99b4 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/Vm.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.openapi.util.UserDataHolderEx +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.nullPromise + +interface AttachStateManager { + fun detach(): Promise<*> = nullPromise() + + val isAttached: Boolean + get() = true +} + +interface Vm : UserDataHolderEx { + val debugListener: DebugEventListener + + val attachStateManager: AttachStateManager + + val evaluateContext: EvaluateContext? + + val scriptManager: ScriptManager + + val breakpointManager: BreakpointManager + + val suspendContextManager: SuspendContextManager<out CallFrame> + + /** + * Controls whether VM stops on exceptions + */ + fun setBreakOnException(catchMode: ExceptionCatchMode): Promise<*> = nullPromise() + + val presentableName: String + get() = "main loop" + + val childVMs: MutableList<Vm> +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/VmBase.kt b/platform/script-debugger/backend/src/debugger/VmBase.kt new file mode 100644 index 00000000..6af35c76 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/VmBase.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.openapi.util.UserDataHolderBase +import com.intellij.util.containers.ContainerUtil + +abstract class VmBase(override val debugListener: DebugEventListener) : Vm, AttachStateManager, UserDataHolderBase() { + override val evaluateContext: EvaluateContext? by lazy(LazyThreadSafetyMode.NONE) { computeEvaluateContext() } + + override val attachStateManager: AttachStateManager = this + + protected open fun computeEvaluateContext(): EvaluateContext? = null + + override val childVMs: MutableList<Vm> = ContainerUtil.createConcurrentList() +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/Base64VLQ.java b/platform/script-debugger/backend/src/debugger/sourcemap/Base64VLQ.java new file mode 100644 index 00000000..7576f500 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/sourcemap/Base64VLQ.java @@ -0,0 +1,79 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.sourcemap; + +final class Base64VLQ { + private Base64VLQ() { + } + + interface CharIterator { + boolean hasNext(); + char next(); + } + + // A Base64 VLQ digit can represent 5 bits, so it is base-32. + private static final int VLQ_BASE_SHIFT = 5; + private static final int VLQ_BASE = 1 << VLQ_BASE_SHIFT; + + // A mask of bits for a VLQ digit (11111), 31 decimal. + private static final int VLQ_BASE_MASK = VLQ_BASE - 1; + + // The continuation bit is the 6th bit. + private static final int VLQ_CONTINUATION_BIT = VLQ_BASE; + + /** + * Decodes the next VLQValue from the provided CharIterator. + */ + public static int decode(CharIterator in) { + int result = 0; + int shift = 0; + int digit; + do { + digit = Base64.BASE64_DECODE_MAP[in.next()]; + assert (digit != -1) : "invalid char"; + + result += (digit & VLQ_BASE_MASK) << shift; + shift += VLQ_BASE_SHIFT; + } + while ((digit & VLQ_CONTINUATION_BIT) != 0); + + boolean negate = (result & 1) == 1; + result >>= 1; + return negate ? -result : result; + } + + private static final class Base64 { + /** + * A map used to convert integer values in the range 0-63 to their base64 + * values. + */ + private static final String BASE64_MAP = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "abcdefghijklmnopqrstuvwxyz" + + "0123456789+/"; + + /** + * A map used to convert base64 character into integer values. + */ + private static final int[] BASE64_DECODE_MAP = new int[256]; + + static { + for (int i = 0; i < BASE64_MAP.length(); i++) { + BASE64_DECODE_MAP[BASE64_MAP.charAt(i)] = i; + } + } + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/MappingEntry.kt b/platform/script-debugger/backend/src/debugger/sourcemap/MappingEntry.kt new file mode 100644 index 00000000..8f9aad86 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/sourcemap/MappingEntry.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.sourcemap + +/** + * Mapping entry in the source map + */ +interface MappingEntry { + val generatedColumn: Int + + val generatedLine: Int + + val sourceLine: Int + + val sourceColumn: Int + + val source: Int + get() = -1 + + val name: String? + get() = null + + val nextGenerated: MappingEntry +} diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/MappingList.kt b/platform/script-debugger/backend/src/debugger/sourcemap/MappingList.kt new file mode 100644 index 00000000..c87786c0 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/sourcemap/MappingList.kt @@ -0,0 +1,210 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.sourcemap + +import com.intellij.openapi.editor.Document +import java.util.* + +interface Mappings { + fun get(line: Int, column: Int): MappingEntry? + + fun getNextOnTheSameLine(index: Int, skipIfColumnEquals: Boolean = true): MappingEntry? + + fun getNext(mapping: MappingEntry): MappingEntry? + + fun getNextOnTheSameLine(mapping: MappingEntry): MappingEntry? { + val nextMapping = getNext(mapping) + return if (nextMapping != null && getLine(nextMapping) == getLine(mapping)) nextMapping else null + } + + fun indexOf(line: Int, column: Int): Int + + fun getByIndex(index: Int): MappingEntry + + fun getLine(mapping: MappingEntry): Int + + fun getColumn(mapping: MappingEntry): Int +} + +abstract class MappingList(private val mappings: List<MappingEntry>) : Mappings { + val size: Int + get() = mappings.size + + protected abstract val comparator: Comparator<MappingEntry> + + override fun indexOf(line: Int, column: Int): Int { + var low = 0 + var high = mappings.size - 1 + if (mappings.isEmpty() || getLine(mappings[low]) > line || getLine(mappings[high]) < line) { + return -1 + } + + while (low <= high) { + val middle = (low + high).ushr(1) + val mapping = mappings[middle] + val mappingLine = getLine(mapping) + if (line == mappingLine) { + if (column == getColumn(mapping)) { + // find first + var firstIndex = middle + while (firstIndex > 0) { + val prevMapping = mappings[firstIndex - 1] + if (getLine(prevMapping) == line && getColumn(prevMapping) == column) { + firstIndex-- + } + else { + break + } + } + return firstIndex + } + else if (column < getColumn(mapping)) { + if (column == 0 || column == -1) { + // find first + var firstIndex = middle + while (firstIndex > 0 && getLine(mappings[firstIndex - 1]) == line) { + firstIndex-- + } + return firstIndex + } + + if (middle == 0) { + return -1 + } + + val prevMapping = mappings[middle - 1] + when { + line != getLine(prevMapping) -> return -1 + column >= getColumn(prevMapping) -> return middle - 1 + else -> high = middle - 1 + } + } + else { + // https://code.google.com/p/google-web-toolkit/issues/detail?id=9103 + // We skipIfColumnEquals because GWT has two entries - source position equals, but generated no. We must use first entry (at least, in case of GWT it is correct) + val nextMapping = getNextOnTheSameLine(middle) + if (nextMapping == null) { + return middle + } + else { + low = middle + 1 + } + } + } + else if (line > mappingLine) { + low = middle + 1 + } + else { + high = middle - 1 + } + } + + return -1 + } + + // todo honor Google Chrome bug related to paused location + override fun get(line: Int, column: Int): MappingEntry? = mappings.getOrNull(indexOf(line, column)) + + private fun getNext(index: Int) = mappings.getOrNull(index + 1) + + override fun getNext(mapping: MappingEntry): MappingEntry? { + if (comparator == MAPPING_COMPARATOR_BY_GENERATED_POSITION) { + return mapping.nextGenerated + } + + var index = mappings.binarySearch(mapping, comparator) + if (index < 0) { + return null + } + index++ + + var result: MappingEntry? + do { + result = mappings.getOrNull(index++) + } + while (mapping === result) + return result + } + + override fun getNextOnTheSameLine(index: Int, skipIfColumnEquals: Boolean): MappingEntry? { + var nextMapping = getNext(index) ?: return null + + val mapping = getByIndex(index) + if (getLine(nextMapping) != getLine(mapping)) { + return null + } + + if (skipIfColumnEquals) { + var i = index + // several generated segments can point to one source segment, so, in mapping list ordered by source, could be several mappings equal in terms of source position + while (getColumn(nextMapping) == getColumn(mapping)) { + nextMapping = getNextOnTheSameLine(++i, false) ?: return null + } + } + + return nextMapping + } + + fun getEndOffset(mapping: MappingEntry, lineStartOffset: Int, document: Document): Int { + val nextMapping = getNextOnTheSameLine(Collections.binarySearch(mappings, mapping, comparator)) + return if (nextMapping == null) document.getLineEndOffset(getLine(mapping)) else lineStartOffset + getColumn(nextMapping) + } + + override fun getByIndex(index: Int): MappingEntry = mappings.get(index) + + // entries will be processed in this list order + fun processMappingsInLine(line: Int, entryProcessor: MappingsProcessorInLine): Boolean { + var low = 0 + var high = mappings.size - 1 + while (low <= high) { + val middle = (low + high).ushr(1) + val mapping = mappings.get(middle) + val mappingLine = getLine(mapping) + when { + line == mappingLine -> { + // find first + var firstIndex = middle + while (firstIndex > 0 && getLine(mappings.get(firstIndex - 1)) == line) { + firstIndex-- + } + + var entry: MappingEntry? = mappings.get(firstIndex) + do { + var nextEntry = mappings.getOrNull(++firstIndex) + if (nextEntry != null && getLine(nextEntry) != line) { + nextEntry = null + } + + if (!entryProcessor.process(entry!!, nextEntry)) { + return true + } + + entry = nextEntry + } + while (entry != null) + return true + } + line > mappingLine -> low = middle + 1 + else -> high = middle - 1 + } + } + return false + } +} + +interface MappingsProcessorInLine { + fun process(entry: MappingEntry, nextEntry: MappingEntry?): Boolean +} diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/NestedSourceMap.kt b/platform/script-debugger/backend/src/debugger/sourcemap/NestedSourceMap.kt new file mode 100644 index 00000000..c4a5ebfc --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/sourcemap/NestedSourceMap.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.sourcemap + +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.Url +import gnu.trove.THashMap + +class NestedSourceMap(private val childMap: SourceMap, private val parentMap: SourceMap) : SourceMap { + override val sourceResolver: SourceResolver + get() = parentMap.sourceResolver + + override val sources: Array<Url> + get() = parentMap.sources + + private val sourceIndexToSourceMappings = arrayOfNulls<Mappings>(parentMap.sources.size) + + private val childMappingToTransformed = THashMap<MappingEntry, MappingEntry>() + + override val outFile: String? + get() = childMap.outFile + + override val hasNameMappings: Boolean + get() = childMap.hasNameMappings || parentMap.hasNameMappings + + override val generatedMappings: Mappings by lazy { + NestedMappings(childMap.generatedMappings, parentMap.generatedMappings, false) + } + + override fun findSourceMappings(sourceIndex: Int): Mappings { + var result = sourceIndexToSourceMappings.get(sourceIndex) + if (result == null) { + result = NestedMappings(childMap.findSourceMappings(sourceIndex), parentMap.findSourceMappings(sourceIndex), true) + sourceIndexToSourceMappings.set(sourceIndex, result) + } + return result + } + + override fun findSourceIndex(sourceFile: VirtualFile, localFileUrlOnly: Boolean): Int = parentMap.findSourceIndex(sourceFile, localFileUrlOnly) + + override fun findSourceIndex(sourceUrls: List<Url>, + sourceFile: VirtualFile?, + resolver: Lazy<SourceFileResolver?>?, + localFileUrlOnly: Boolean): Int = parentMap.findSourceIndex(sourceUrls, sourceFile, resolver, localFileUrlOnly) + + override fun processSourceMappingsInLine(sourceIndex: Int, sourceLine: Int, mappingProcessor: MappingsProcessorInLine): Boolean { + val childSourceMappings = childMap.findSourceMappings(sourceIndex) + return (parentMap.findSourceMappings(sourceIndex) as MappingList).processMappingsInLine(sourceLine, object: MappingsProcessorInLine { + override fun process(entry: MappingEntry, nextEntry: MappingEntry?): Boolean { + val childIndex = childSourceMappings.indexOf(entry.generatedLine, entry.generatedColumn) + if (childIndex == -1) { + return true + } + + val childEntry = childSourceMappings.getByIndex(childIndex) + // todo not clear - should we resolve next child entry by current child index or by provided parent nextEntry? + val nextChildEntry = if (nextEntry == null) null else childSourceMappings.getNextOnTheSameLine(childIndex) + return mappingProcessor.process(childMappingToTransformed.getOrPut(childEntry) { NestedMappingEntry(childEntry, entry) }, + nextChildEntry?.let { childMappingToTransformed.getOrPut(it) { NestedMappingEntry(it, entry) } }) + } + }) + } +} + +private class NestedMappings(private val child: Mappings, private val parent: Mappings, private val isSourceMappings: Boolean) : Mappings { + override fun getNextOnTheSameLine(index: Int, skipIfColumnEquals: Boolean) = parent.getNextOnTheSameLine(index, skipIfColumnEquals) + + override fun getNext(mapping: MappingEntry) = parent.getNext(mapping) + + override fun indexOf(line: Int, column: Int) = parent.indexOf(line, column) + + override fun getByIndex(index: Int) = parent.getByIndex(index) + + override fun getLine(mapping: MappingEntry) = parent.getLine(mapping) + + override fun getColumn(mapping: MappingEntry) = parent.getColumn(mapping) + + override fun get(line: Int, column: Int): MappingEntry? { + return if (isSourceMappings) { + parent.get(line, column)?.let { child.get(it.generatedLine, it.generatedColumn) } + } + else { + child.get(line, column)?.let { parent.get(it.sourceLine, it.sourceColumn) } + } + } +} + +private data class NestedMappingEntry(private val child: MappingEntry, private val parent: MappingEntry) : MappingEntry { + override val generatedLine: Int + get() = child.generatedLine + + override val generatedColumn: Int + get() = child.generatedColumn + + override val sourceLine: Int + get() = parent.sourceLine + + override val sourceColumn: Int + get() = parent.sourceColumn + + override val name: String? + get() = parent.name + + override val nextGenerated: MappingEntry + get() = child.nextGenerated +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/SourceMap.kt b/platform/script-debugger/backend/src/debugger/sourcemap/SourceMap.kt new file mode 100644 index 00000000..283a5b6c --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/sourcemap/SourceMap.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.sourcemap + +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.Url + +// sources - is not originally specified, but canonicalized/normalized +// lines and columns are zero-based according to specification +interface SourceMap { + val outFile: String? + + /** + * note: Nested map returns only parent sources + */ + val sources: Array<Url> + + val generatedMappings: Mappings + val hasNameMappings: Boolean + val sourceResolver: SourceResolver + + fun findSourceMappings(sourceIndex: Int): Mappings + + fun findSourceIndex(sourceUrls: List<Url>, sourceFile: VirtualFile?, resolver: Lazy<SourceFileResolver?>?, localFileUrlOnly: Boolean): Int + + fun findSourceMappings(sourceUrls: List<Url>, sourceFile: VirtualFile?, resolver: Lazy<SourceFileResolver?>?, localFileUrlOnly: Boolean): Mappings? { + val sourceIndex = findSourceIndex(sourceUrls, sourceFile, resolver, localFileUrlOnly) + return if (sourceIndex >= 0) findSourceMappings(sourceIndex) else null + } + + fun getSourceLineByRawLocation(rawLine: Int, rawColumn: Int): Int = generatedMappings.get(rawLine, rawColumn)?.sourceLine ?: -1 + + fun findSourceIndex(sourceFile: VirtualFile, localFileUrlOnly: Boolean): Int + + fun processSourceMappingsInLine(sourceIndex: Int, sourceLine: Int, mappingProcessor: MappingsProcessorInLine): Boolean + + fun processSourceMappingsInLine(sourceUrls: List<Url>, sourceLine: Int, mappingProcessor: MappingsProcessorInLine, sourceFile: VirtualFile?, resolver: Lazy<SourceFileResolver?>?, localFileUrlOnly: Boolean): Boolean { + val sourceIndex = findSourceIndex(sourceUrls, sourceFile, resolver, localFileUrlOnly) + return sourceIndex >= 0 && processSourceMappingsInLine(sourceIndex, sourceLine, mappingProcessor) + } +} + + +class OneLevelSourceMap(override val outFile: String?, + override val generatedMappings: Mappings, + private val sourceIndexToMappings: Array<MappingList?>, + override val sourceResolver: SourceResolver, + override val hasNameMappings: Boolean) : SourceMap { + override val sources: Array<Url> + get() = sourceResolver.canonicalizedUrls + + override fun findSourceIndex(sourceUrls: List<Url>, sourceFile: VirtualFile?, resolver: Lazy<SourceFileResolver?>?, localFileUrlOnly: Boolean): Int { + val index = sourceResolver.findSourceIndex(sourceUrls, sourceFile, localFileUrlOnly) + if (index == -1 && resolver != null) { + return resolver.value?.let { sourceResolver.findSourceIndex(it) } ?: -1 + } + return index + } + + // returns SourceMappingList + override fun findSourceMappings(sourceIndex: Int): MappingList = sourceIndexToMappings.get(sourceIndex)!! + + override fun findSourceIndex(sourceFile: VirtualFile, localFileUrlOnly: Boolean): Int = sourceResolver.findSourceIndexByFile(sourceFile, localFileUrlOnly) + + override fun processSourceMappingsInLine(sourceIndex: Int, sourceLine: Int, mappingProcessor: MappingsProcessorInLine): Boolean { + return findSourceMappings(sourceIndex).processMappingsInLine(sourceLine, mappingProcessor) + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/SourceMapDecoder.kt b/platform/script-debugger/backend/src/debugger/sourcemap/SourceMapDecoder.kt new file mode 100644 index 00000000..3ecdd792 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/sourcemap/SourceMapDecoder.kt @@ -0,0 +1,372 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.sourcemap + +import com.google.gson.stream.JsonToken +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.util.registry.Registry +import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.util.text.StringUtilRt +import com.intellij.util.PathUtil +import com.intellij.util.SmartList +import com.intellij.util.UriUtil +import com.intellij.util.containers.isNullOrEmpty +import org.jetbrains.debugger.sourcemap.Base64VLQ.CharIterator +import org.jetbrains.io.JsonReaderEx +import java.io.IOException +import java.util.* +import kotlin.properties.Delegates.notNull + +private val MAPPING_COMPARATOR_BY_SOURCE_POSITION = Comparator<MappingEntry> { o1, o2 -> + if (o1.sourceLine == o2.sourceLine) { + o1.sourceColumn - o2.sourceColumn + } + else { + o1.sourceLine - o2.sourceLine + } +} + +val MAPPING_COMPARATOR_BY_GENERATED_POSITION: Comparator<MappingEntry> = Comparator { o1, o2 -> + if (o1.generatedLine == o2.generatedLine) { + o1.generatedColumn - o2.generatedColumn + } + else { + o1.generatedLine - o2.generatedLine + } +} + +internal const val UNMAPPED = -1 + +// https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?hl=en_US +fun decodeSourceMap(`in`: CharSequence, sourceResolverFactory: (sourceUrls: List<String>, sourceContents: List<String?>?) -> SourceResolver): SourceMap? { + if (`in`.isEmpty()) { + throw IOException("source map contents cannot be empty") + } + + val reader = JsonReaderEx(`in`) + reader.isLenient = true + return parseMap(reader, 0, 0, ArrayList(), sourceResolverFactory) +} + +private fun parseMap(reader: JsonReaderEx, + line: Int, + column: Int, + mappings: MutableList<MappingEntry>, + sourceResolverFactory: (sourceUrls: List<String>, sourceContents: List<String?>?) -> SourceResolver): SourceMap? { + reader.beginObject() + var sourceRoot: String? = null + var sourcesReader: JsonReaderEx? = null + var names: List<String>? = null + var encodedMappings: String? = null + var file: String? = null + var version = -1 + var sourcesContent: MutableList<String?>? = null + while (reader.hasNext()) { + when (reader.nextName()) { + "sections" -> throw IOException("sections is not supported yet") + "version" -> { + version = reader.nextInt() + } + "sourceRoot" -> { + sourceRoot = StringUtil.nullize(readSourcePath(reader)) + if (sourceRoot != null && sourceRoot != "/") { + sourceRoot = UriUtil.trimTrailingSlashes(sourceRoot) + } + } + "sources" -> { + sourcesReader = reader.subReader() + reader.skipValue() + } + "names" -> { + reader.beginArray() + if (reader.hasNext()) { + names = ArrayList() + do { + if (reader.peek() == JsonToken.BEGIN_OBJECT) { + // polymer map + reader.skipValue() + names.add("POLYMER UNKNOWN NAME") + } + else { + names.add(reader.nextString(true)) + } + } + while (reader.hasNext()) + } + else { + names = emptyList() + } + reader.endArray() + } + "mappings" -> { + encodedMappings = reader.nextString() + } + "file" -> { + file = reader.nextNullableString() + } + "sourcesContent" -> { + reader.beginArray() + if (reader.peek() != JsonToken.END_ARRAY) { + sourcesContent = SmartList<String>() + do { + if (reader.peek() == JsonToken.STRING) { + sourcesContent.add(StringUtilRt.convertLineSeparators(reader.nextString())) + } + else if (reader.peek() == JsonToken.NULL) { + // null means source file should be resolved by url + sourcesContent.add(null) + reader.nextNull() + } + else { + logger<SourceMap>().warn("Unknown sourcesContent element: ${reader.peek().name}") + reader.skipValue() + } + } + while (reader.hasNext()) + } + reader.endArray() + } + else -> { + // skip file or extensions + reader.skipValue() + } + } + } + reader.close() + + // check it before other checks, probably it is not a sourcemap file + if (encodedMappings.isNullOrEmpty()) { + // empty map + return null + } + + if (Registry.`is`("js.debugger.fix.jspm.source.maps", false) && encodedMappings!!.startsWith(";") && file != null && file.endsWith(".ts!transpiled")) { + encodedMappings = encodedMappings.substring(1) + } + + if (version != 3) { + throw IOException("Unsupported sourcemap version: $version") + } + + if (sourcesReader == null) { + throw IOException("sources is not specified") + } + + val sources = readSources(sourcesReader, sourceRoot) + if (sources.isEmpty()) { + // empty map, meteor can report such ugly maps + return null + } + + val reverseMappingsBySourceUrl = arrayOfNulls<MutableList<MappingEntry>?>(sources.size) + readMappings(encodedMappings!!, line, column, mappings, reverseMappingsBySourceUrl, names) + + val sourceToEntries = Array<MappingList?>(reverseMappingsBySourceUrl.size) { + val entries = reverseMappingsBySourceUrl[it] + if (entries == null) { + null + } + else { + entries.sortWith(MAPPING_COMPARATOR_BY_SOURCE_POSITION) + SourceMappingList(entries) + } + } + return OneLevelSourceMap(file, GeneratedMappingList(mappings), sourceToEntries, sourceResolverFactory(sources, sourcesContent), !names.isNullOrEmpty()) +} + +private fun readSourcePath(reader: JsonReaderEx): String = PathUtil.toSystemIndependentName(reader.nextString().trim { it <= ' ' }) + +private fun readMappings(value: String, + initialLine: Int, + initialColumn: Int, + mappings: MutableList<MappingEntry>, + reverseMappingsBySourceUrl: Array<MutableList<MappingEntry>?>, + names: List<String>?) { + if (value.isEmpty()) { + return + } + + var line = initialLine + var column = initialColumn + val charIterator = CharSequenceIterator(value) + var sourceIndex = 0 + var reverseMappings: MutableList<MappingEntry> = getMapping(reverseMappingsBySourceUrl, sourceIndex) + var sourceLine = 0 + var sourceColumn = 0 + var nameIndex = 0 + var prevEntry: MutableEntry? = null + + fun addEntry(entry: MutableEntry) { + if (prevEntry != null) { + prevEntry!!.nextGenerated = entry + } + prevEntry = entry + mappings.add(entry) + } + + while (charIterator.hasNext()) { + if (charIterator.peek() == ',') { + charIterator.next() + } + else { + while (charIterator.peek() == ';') { + line++ + column = 0 + charIterator.next() + if (!charIterator.hasNext()) { + return + } + } + } + + column += Base64VLQ.decode(charIterator) + if (isSeparator(charIterator)) { + addEntry(UnmappedEntry(line, column)) + continue + } + + val sourceIndexDelta = Base64VLQ.decode(charIterator) + if (sourceIndexDelta != 0) { + sourceIndex += sourceIndexDelta + reverseMappings = getMapping(reverseMappingsBySourceUrl, sourceIndex) + } + sourceLine += Base64VLQ.decode(charIterator) + sourceColumn += Base64VLQ.decode(charIterator) + + val entry: MutableEntry + if (isSeparator(charIterator)) { + entry = UnnamedEntry(line, column, sourceIndex, sourceLine, sourceColumn) + } + else { + nameIndex += Base64VLQ.decode(charIterator) + assert(names != null) + entry = NamedEntry(names!![nameIndex], line, column, sourceIndex, sourceLine, sourceColumn) + } + reverseMappings.add(entry) + addEntry(entry) + } +} + +private fun readSources(reader: JsonReaderEx, sourceRoot: String?): List<String> { + reader.beginArray() + val sources: List<String> + if (reader.peek() == JsonToken.END_ARRAY) { + sources = emptyList() + } + else { + sources = SmartList<String>() + do { + var sourceUrl: String = readSourcePath(reader) + if (!sourceRoot.isNullOrEmpty()) { + if (sourceRoot == "/") { + sourceUrl = "/$sourceUrl" + } + else { + sourceUrl = "$sourceRoot/$sourceUrl" + } + } + sources.add(sourceUrl) + } + while (reader.hasNext()) + } + reader.endArray() + return sources +} + +private fun getMapping(reverseMappingsBySourceUrl: Array<MutableList<MappingEntry>?>, sourceIndex: Int): MutableList<MappingEntry> { + var reverseMappings = reverseMappingsBySourceUrl.get(sourceIndex) + if (reverseMappings == null) { + reverseMappings = ArrayList() + reverseMappingsBySourceUrl.set(sourceIndex, reverseMappings) + } + return reverseMappings +} + +private fun isSeparator(charIterator: CharSequenceIterator): Boolean { + if (!charIterator.hasNext()) { + return true + } + + val current = charIterator.peek() + return current == ',' || current == ';' +} + +interface MutableEntry : MappingEntry { + override var nextGenerated: MappingEntry +} + +/** + * Not mapped to a section in the original source. + */ +private data class UnmappedEntry(override val generatedLine: Int, override val generatedColumn: Int) : MappingEntry, MutableEntry { + override val sourceLine = UNMAPPED + + override val sourceColumn = UNMAPPED + + override var nextGenerated: MappingEntry by notNull() +} + +/** + * Mapped to a section in the original source. + */ +private data class UnnamedEntry(override val generatedLine: Int, + override val generatedColumn: Int, + override val source: Int, + override val sourceLine: Int, + override val sourceColumn: Int) : MappingEntry, MutableEntry { + override var nextGenerated: MappingEntry by notNull() +} + +/** + * Mapped to a section in the original source, and is associated with a name. + */ +private data class NamedEntry(override val name: String, + override val generatedLine: Int, + override val generatedColumn: Int, + override val source: Int, + override val sourceLine: Int, + override val sourceColumn: Int) : MappingEntry, MutableEntry { + override var nextGenerated: MappingEntry by notNull() +} + +// java CharacterIterator is ugly, next() impl, so, we reinvent +private class CharSequenceIterator(private val content: CharSequence) : CharIterator { + private val length = content.length + private var current = 0 + + override fun next() = content.get(current++) + + internal fun peek() = content.get(current) + + override fun hasNext() = current < length +} + +private class SourceMappingList(mappings: List<MappingEntry>) : MappingList(mappings) { + override fun getLine(mapping: MappingEntry) = mapping.sourceLine + + override fun getColumn(mapping: MappingEntry) = mapping.sourceColumn + + override val comparator = MAPPING_COMPARATOR_BY_SOURCE_POSITION +} + +private class GeneratedMappingList(mappings: List<MappingEntry>) : MappingList(mappings) { + override fun getLine(mapping: MappingEntry) = mapping.generatedLine + + override fun getColumn(mapping: MappingEntry) = mapping.generatedColumn + + override val comparator = MAPPING_COMPARATOR_BY_GENERATED_POSITION +} + diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/SourceResolver.kt b/platform/script-debugger/backend/src/debugger/sourcemap/SourceResolver.kt new file mode 100644 index 00000000..2a9c868f --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/sourcemap/SourceResolver.kt @@ -0,0 +1,186 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger.sourcemap + +import com.intellij.openapi.util.SystemInfo +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.vfs.StandardFileSystems +import com.intellij.openapi.vfs.VfsUtilCore +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.Url +import com.intellij.util.Urls +import com.intellij.util.containers.ObjectIntHashMap +import com.intellij.util.containers.isNullOrEmpty +import com.intellij.util.io.URLUtil +import java.io.File + +interface SourceFileResolver { + /** + * Return -1 if no match + */ + fun resolve(map: ObjectIntHashMap<Url>): Int = -1 + fun resolve(rawSources: List<String>): Int = -1 +} + +class SourceResolver(private val rawSources: List<String>, + trimFileScheme: Boolean, + baseUrl: Url?, + private val sourceContents: List<String?>?, + baseUrlIsFile: Boolean = true) { + companion object { + fun isAbsolute(path: String): Boolean = path.startsWith('/') || (SystemInfo.isWindows && (path.length > 2 && path[1] == ':')) + } + + val canonicalizedUrls: Array<Url> by lazy { + Array(rawSources.size) { canonicalizeUrl(rawSources[it], baseUrl, trimFileScheme, baseUrlIsFile) } + } + + private val canonicalizedUrlToSourceIndex: ObjectIntHashMap<Url> by lazy { + ( + if (SystemInfo.isFileSystemCaseSensitive) ObjectIntHashMap(rawSources.size) + else ObjectIntHashMap(rawSources.size, Urls.caseInsensitiveUrlHashingStrategy) + ).also { + for (i in rawSources.indices) { + it.put(canonicalizedUrls[i], i) + } + } + } + + fun getSource(entry: MappingEntry): Url? { + val index = entry.source + return if (index < 0) null else canonicalizedUrls[index] + } + + fun getSourceContent(entry: MappingEntry): String? { + if (sourceContents.isNullOrEmpty()) { + return null + } + + val index = entry.source + return if (index < 0 || index >= sourceContents!!.size) null else sourceContents[index] + } + + fun getSourceContent(sourceIndex: Int): String? { + if (sourceContents.isNullOrEmpty()) { + return null + } + return if (sourceIndex < 0 || sourceIndex >= sourceContents!!.size) null else sourceContents[sourceIndex] + } + + fun getSourceIndex(url: Url): Int = canonicalizedUrlToSourceIndex[url] + + fun getRawSource(entry: MappingEntry): String? { + val index = entry.source + return if (index < 0) null else rawSources[index] + } + + internal fun findSourceIndex(resolver: SourceFileResolver): Int { + val resolveByCanonicalizedUrls = resolver.resolve(canonicalizedUrlToSourceIndex) + return if (resolveByCanonicalizedUrls != -1) resolveByCanonicalizedUrls else resolver.resolve(rawSources) + } + + fun findSourceIndex(sourceUrls: List<Url>, sourceFile: VirtualFile?, localFileUrlOnly: Boolean): Int { + for (sourceUrl in sourceUrls) { + val index = canonicalizedUrlToSourceIndex.get(sourceUrl) + if (index != -1) { + return index + } + } + + if (sourceFile != null) { + return findSourceIndexByFile(sourceFile, localFileUrlOnly) + } + return -1 + } + + internal fun findSourceIndexByFile(sourceFile: VirtualFile, localFileUrlOnly: Boolean): Int { + if (!localFileUrlOnly) { + val index = canonicalizedUrlToSourceIndex.get(Urls.newFromVirtualFile(sourceFile).trimParameters()) + if (index != -1) { + return index + } + } + + if (!sourceFile.isInLocalFileSystem) { + return -1 + } + + // local file url - without "file" scheme, just path + val index = canonicalizedUrlToSourceIndex.get(Urls.newLocalFileUrl(sourceFile)) + if (index != -1) { + return index + } + + // ok, search by canonical path + val canonicalFile = sourceFile.canonicalFile + if (canonicalFile != null && canonicalFile != sourceFile) { + for (i in canonicalizedUrls.indices) { + val url = canonicalizedUrls.get(i) + if (Urls.equalsIgnoreParameters(url, canonicalFile)) { + return i + } + } + } + return -1 + } + + fun getUrlIfLocalFile(entry: MappingEntry): Url? = canonicalizedUrls.getOrNull(entry.source)?.let { if (it.isInLocalFileSystem) it else null } +} + +fun canonicalizePath(url: String, baseUrl: Url, baseUrlIsFile: Boolean): String { + var path = url + if (!FileUtil.isAbsolute(url) && !url.isEmpty() && url[0] != '/') { + val basePath = baseUrl.path + if (baseUrlIsFile) { + val lastSlashIndex = basePath.lastIndexOf('/') + val pathBuilder = StringBuilder() + if (lastSlashIndex == -1) { + pathBuilder.append('/') + } + else { + pathBuilder.append(basePath, 0, lastSlashIndex + 1) + } + path = pathBuilder.append(url).toString() + } + else { + path = "$basePath/$url" + } + } + return FileUtil.toCanonicalPath(path, '/') +} + +// see canonicalizeUri kotlin impl and https://trac.webkit.org/browser/trunk/Source/WebCore/inspector/front-end/ParsedURL.js completeURL +fun canonicalizeUrl(url: String, baseUrl: Url?, trimFileScheme: Boolean, baseUrlIsFile: Boolean = true): Url { + if (trimFileScheme && url.startsWith(StandardFileSystems.FILE_PROTOCOL_PREFIX)) { + return Urls.newLocalFileUrl(FileUtil.toCanonicalPath(VfsUtilCore.toIdeaUrl(url, true).substring(StandardFileSystems.FILE_PROTOCOL_PREFIX.length), '/')) + } + else if (baseUrl == null || url.contains(URLUtil.SCHEME_SEPARATOR) || url.startsWith("data:") || url.startsWith("blob:") || + url.startsWith("javascript:") || url.startsWith("webpack:")) { + // consider checking :/ instead of :// because scheme may be followed by path, not by authority + // https://tools.ietf.org/html/rfc3986#section-1.1.2 + // be careful with windows paths: C:/Users + return Urls.parseEncoded(url) ?: Urls.newUri(null, url) + } + else { + return doCanonicalize(url, baseUrl, baseUrlIsFile, true) + } +} + +fun doCanonicalize(url: String, baseUrl: Url, baseUrlIsFile: Boolean, asLocalFileIfAbsoluteAndExists: Boolean): Url { + val path = canonicalizePath(url, baseUrl, baseUrlIsFile) + if ((baseUrl.scheme == null && baseUrl.isInLocalFileSystem) || + asLocalFileIfAbsoluteAndExists && SourceResolver.isAbsolute(path) && File(path).exists()) { + // file:///home/user/foo.js.map, foo.ts -> /home/user/foo.ts (baseUrl is in local fs) + // http://localhost/home/user/foo.js.map, foo.ts -> /home/user/foo.ts (File(path) exists) + return Urls.newLocalFileUrl(path) + } + else if (!path.startsWith("/")) { + // http://localhost/source.js.map, C:/foo.ts webpack-dsj3c45 -> C:/foo.ts webpack-dsj3c45 + // (we can't append path suffixes unless they start with / + return Urls.parse(path, true) ?: Urls.newUnparsable(path) + } + else { + // new url from path and baseUrl's scheme and authority + val split = path.split('?', limit = 2) + return Urls.newUrl(baseUrl.scheme!!, baseUrl.authority!!, split[0], if (split.size > 1) '?' + split[1] else null) + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/util.kt b/platform/script-debugger/backend/src/debugger/util.kt new file mode 100644 index 00000000..7930261d --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/util.kt @@ -0,0 +1,112 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.util.io.FileUtilRt +import com.intellij.openapi.util.registry.Registry +import com.intellij.util.concurrency.AppExecutorUtil +import com.intellij.util.io.CharSequenceBackedByChars +import com.intellij.util.io.addChannelListener +import io.netty.buffer.ByteBuf +import io.netty.channel.Channel +import org.jetbrains.annotations.PropertyKey +import java.io.File +import java.io.FileOutputStream +import java.nio.CharBuffer +import java.text.SimpleDateFormat +import java.util.concurrent.Future +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.TimeUnit + +internal class LogEntry(val message: CharSequence, val marker: String) { + internal val time = System.currentTimeMillis() +} + +class MessagingLogger internal constructor(debugFile: String) { + private val processFuture: Future<*> + private val queue = LinkedBlockingQueue<LogEntry>() + + init { + processFuture = ApplicationManager.getApplication().executeOnPooledThread { + val file = File(FileUtil.expandUserHome(debugFile)) + FileUtilRt.createParentDirs(file) + val out = FileOutputStream(file) + val writer = out.writer() + writer.write("[\n") + writer.flush() + val fileChannel = out.channel + + val dateFormatter = SimpleDateFormat("HH.mm.ss,SSS") + + try { + while (true) { + val entry = queue.take() + + writer.write("""{"timestamp": "${dateFormatter.format(entry.time)}", """) + val message = entry.message + writer.write("\"${entry.marker}\": ") + writer.flush() + + if (message is CharSequenceBackedByChars) { + fileChannel.write(message.byteBuffer) + } + else { + fileChannel.write(Charsets.UTF_8.encode(CharBuffer.wrap(message))) + } + + writer.write("},\n") + writer.flush() + } + } + catch (e: InterruptedException) { + } + finally { + writer.write("]") + writer.flush() + out.close() + } + } + } + + fun add(message: CharSequence, marker: String = "IN") { + // ignore Network events + if (!message.startsWith("{\"method\":\"Network.")) { + queue.add(LogEntry(message, marker)) + } + } + + fun add(outMessage: ByteBuf, marker: String = "OUT") { + val charSequence = outMessage.getCharSequence(outMessage.readerIndex(), outMessage.readableBytes(), Charsets.UTF_8) + add(charSequence, marker) + } + + fun close() { + AppExecutorUtil.getAppScheduledExecutorService().schedule(fun() { + processFuture.cancel(true) + }, 1, TimeUnit.SECONDS) + } + + fun closeOnChannelClose(channel: Channel) { + channel.closeFuture().addChannelListener { + try { + add("\"Closed\"", "Channel") + } + finally { + close() + } + } + } +} + +fun createDebugLogger(@PropertyKey(resourceBundle = Registry.REGISTRY_BUNDLE) key: String, suffix: String = ""): MessagingLogger? { + var debugFile = Registry.stringValue(key) + if (debugFile.isEmpty()) { + return null + } + + if (!suffix.isEmpty()) { + debugFile = debugFile.replace(".json", "$suffix.json") + } + return MessagingLogger(debugFile) +} diff --git a/platform/script-debugger/backend/src/debugger/values/ArrayValue.kt b/platform/script-debugger/backend/src/debugger/values/ArrayValue.kt new file mode 100644 index 00000000..ceed459a --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/values/ArrayValue.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.values + +interface ArrayValue : ObjectValue { + /** + * Be aware - it is not equals to java array length. + * In case of sparse array `var sparseArray = [3, 4]; + * sparseArray[45] = 34; + * sparseArray[40999995] = "foo"; + ` * + * length will be equal to 40999995. + */ + val length: Int +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/values/FunctionValue.kt b/platform/script-debugger/backend/src/debugger/values/FunctionValue.kt new file mode 100644 index 00000000..2825bc1e --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/values/FunctionValue.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.values + +import com.intellij.util.ThreeState +import org.jetbrains.concurrency.Promise +import org.jetbrains.debugger.Scope + +interface FunctionValue : ObjectValue { + /** + * You must invoke [.resolve] to use any function value methods + */ + fun resolve(): Promise<FunctionValue> + + /** + * Returns position of opening parenthesis of function arguments. Position is absolute + * within resource (not relative to script start position). + + * @return position or null if position is not available + */ + val openParenLine: Int + + val openParenColumn: Int + + val scopes: Array<Scope>? + + /** + * Method could be called (it is normal and expected) for unresolved function. + * It must return quickly. Return [com.intellij.util.ThreeState.UNSURE] otherwise. + */ + fun hasScopes(): ThreeState = ThreeState.UNSURE +} diff --git a/platform/script-debugger/backend/src/debugger/values/IndexedVariablesConsumer.kt b/platform/script-debugger/backend/src/debugger/values/IndexedVariablesConsumer.kt new file mode 100644 index 00000000..aad2f00d --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/values/IndexedVariablesConsumer.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.values + +import org.jetbrains.debugger.Variable + +abstract class IndexedVariablesConsumer { + // null if array is not sparse + abstract fun consumeRanges(ranges: IntArray?) + + abstract fun consumeVariables(variables: List<Variable>) + + open val isObsolete: Boolean + get() = false +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/values/ObjectValue.kt b/platform/script-debugger/backend/src/debugger/values/ObjectValue.kt new file mode 100755 index 00000000..55917c42 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/values/ObjectValue.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.values + +import com.intellij.util.ThreeState +import org.jetbrains.concurrency.Obsolescent +import org.jetbrains.concurrency.Promise +import org.jetbrains.debugger.EvaluateContext +import org.jetbrains.debugger.Variable +import org.jetbrains.debugger.VariablesHost + +/** + * A compound value that has zero or more properties + */ +interface ObjectValue : Value { + val className: String? + + val properties: Promise<List<Variable>> + + fun getProperties(names: List<String>, evaluateContext: EvaluateContext, obsolescent: Obsolescent): Promise<List<Variable>> + + val variablesHost: VariablesHost<ValueManager> + + /** + * from (inclusive) to (exclusive) ranges of array elements or elements if less than bucketThreshold + + * "to" could be -1 (sometimes length is unknown, so, you can pass -1 instead of actual elements size) + */ + fun getIndexedProperties(from: Int, to: Int, bucketThreshold: Int, consumer: IndexedVariablesConsumer, componentType: ValueType? = null): Promise<*> + + /** + * It must return quickly. Return [com.intellij.util.ThreeState.UNSURE] otherwise. + */ + fun hasProperties(): ThreeState = ThreeState.UNSURE + + /** + * It must return quickly. Return [com.intellij.util.ThreeState.UNSURE] otherwise. + */ + fun hasIndexedProperties(): ThreeState = ThreeState.NO +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/values/ObjectValueBase.kt b/platform/script-debugger/backend/src/debugger/values/ObjectValueBase.kt new file mode 100644 index 00000000..f56adc43 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/values/ObjectValueBase.kt @@ -0,0 +1,66 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger.values + +import com.intellij.util.SmartList +import org.jetbrains.concurrency.* +import org.jetbrains.debugger.EvaluateContext +import org.jetbrains.debugger.Variable +import org.jetbrains.debugger.VariablesHost +import java.util.* + +abstract class ObjectValueBase<VALUE_LOADER : ValueManager>(type: ValueType) : ValueBase(type), ObjectValue { + protected abstract val childrenManager: VariablesHost<VALUE_LOADER> + + override val properties: Promise<List<Variable>> + get() = childrenManager.get() + + internal abstract inner class MyObsolescentAsyncFunction<PARAM, RESULT>(private val obsolescent: Obsolescent) : ObsolescentFunction<PARAM, Promise<RESULT>> { + override fun isObsolete() = obsolescent.isObsolete || childrenManager.valueManager.isObsolete + } + + override fun getProperties(names: List<String>, evaluateContext: EvaluateContext, obsolescent: Obsolescent): Promise<List<Variable>> = properties + .thenAsync(object : MyObsolescentAsyncFunction<List<Variable>, List<Variable>>(obsolescent) { + override fun `fun`(variables: List<Variable>) = getSpecifiedProperties(variables, names, evaluateContext) + }) + + override val valueString: String? = null + + override fun getIndexedProperties(from: Int, to: Int, bucketThreshold: Int, consumer: IndexedVariablesConsumer, componentType: ValueType?): Promise<*> = rejectedPromise<Any?>() + + @Suppress("UNCHECKED_CAST") + override val variablesHost: VariablesHost<ValueManager> + get() = childrenManager as VariablesHost<ValueManager> +} + +fun getSpecifiedProperties(variables: List<Variable>, names: List<String>, evaluateContext: EvaluateContext): Promise<List<Variable>> { + val properties = SmartList<Variable>() + var getterCount = 0 + for (property in variables) { + if (!property.isReadable || !names.contains(property.name)) { + continue + } + + if (!properties.isEmpty()) { + Collections.sort(properties) { o1, o2 -> names.indexOf(o1.name) - names.indexOf(o2.name) } + } + + properties.add(property) + if (property.value == null) { + getterCount++ + } + } + + if (getterCount == 0) { + return resolvedPromise(properties) + } + else { + val promises = SmartList<Promise<*>>() + for (variable in properties) { + if (variable.value == null) { + val valueModifier = variable.valueModifier!! + promises.add(valueModifier.evaluateGet(variable, evaluateContext)) + } + } + return promises.all(properties) + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/values/PrimitiveValue.kt b/platform/script-debugger/backend/src/debugger/values/PrimitiveValue.kt new file mode 100644 index 00000000..995e905c --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/values/PrimitiveValue.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.values + +open class PrimitiveValue(type: ValueType, override val valueString: String) : ValueBase(type) { + + constructor(type: ValueType, value: Int) : this(type, Integer.toString(value)) { + } + + constructor(type: ValueType, value: Long) : this(type, java.lang.Long.toString(value)) { + } + + companion object { + val NA_N_VALUE: String = "NaN" + val INFINITY_VALUE: String = "Infinity" + + @JvmField + val NULL: PrimitiveValue = PrimitiveValue(ValueType.NULL, "null") + @JvmField + val UNDEFINED: PrimitiveValue = PrimitiveValue(ValueType.UNDEFINED, "undefined") + + val NAN: PrimitiveValue = PrimitiveValue(ValueType.NUMBER, NA_N_VALUE) + val INFINITY: PrimitiveValue = PrimitiveValue(ValueType.NUMBER, INFINITY_VALUE) + + private val TRUE = PrimitiveValue(ValueType.BOOLEAN, "true") + private val FALSE = PrimitiveValue(ValueType.BOOLEAN, "false") + + fun bool(value: String): PrimitiveValue { + return if (value == "true") TRUE else FALSE + } + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/values/StringValue.kt b/platform/script-debugger/backend/src/debugger/values/StringValue.kt new file mode 100644 index 00000000..8a1e8c48 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/values/StringValue.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.values + +import org.jetbrains.concurrency.Promise + +interface StringValue : Value { + val isTruncated: Boolean + + val length: Int + + /** + * Asynchronously reloads object value with extended size limit + */ + val fullString: Promise<String> +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/values/Value.kt b/platform/script-debugger/backend/src/debugger/values/Value.kt new file mode 100755 index 00000000..79846ce9 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/values/Value.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.values + +/** + * An object that represents a VM variable value (compound or atomic). + */ +interface Value { + val type: ValueType + + /** + * @return a string representation of this value + */ + val valueString: String? +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/values/ValueBase.kt b/platform/script-debugger/backend/src/debugger/values/ValueBase.kt new file mode 100644 index 00000000..2d62611a --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/values/ValueBase.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.values + +abstract class ValueBase(override val type: ValueType) : Value
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/values/ValueManager.kt b/platform/script-debugger/backend/src/debugger/values/ValueManager.kt new file mode 100644 index 00000000..4d7eb248 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/values/ValueManager.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.values + +import org.jetbrains.concurrency.Obsolescent +import java.util.concurrent.atomic.AtomicInteger + +/** + * The main idea of this class - don't create value for remote value handle if already exists. So, + * implementation of this class keep map of value to remote value handle. + * Also, this class maintains cache timestamp. + + * Currently WIP implementation doesn't keep such map due to protocol issue. But V8 does. + */ +abstract class ValueManager() : Obsolescent { + private val cacheStamp = AtomicInteger() + @Volatile private var obsolete = false + + open fun clearCaches() { + cacheStamp.incrementAndGet() + } + + fun getCacheStamp(): Int = cacheStamp.get() + + final override fun isObsolete(): Boolean = obsolete + + fun markObsolete() { + obsolete = true + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/values/ValueType.kt b/platform/script-debugger/backend/src/debugger/values/ValueType.kt new file mode 100644 index 00000000..591e827b --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/values/ValueType.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger.values + +private val VALUE_TYPES = ValueType.values() + +/** + * Don't forget to update NashornDebuggerSupport.ValueType and DebuggerSupport.ts respectively also + */ +enum class ValueType { + OBJECT, + NUMBER, + STRING, + FUNCTION, + BOOLEAN, + BIGINT, + + ARRAY, + NODE, + + UNDEFINED, + NULL, + SYMBOL; + + /** + * Returns whether `type` corresponds to a JsObject. Note that while 'null' is an object + * in JavaScript world, here for API consistency it has bogus type [.NULL] and is + * not a [ObjectValue] + */ + val isObjectType: Boolean + get() = this == OBJECT || this == ARRAY || this == FUNCTION || this == NODE + + companion object { + fun fromIndex(index: Int): ValueType = VALUE_TYPES.get(index) + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/rpc/CommandProcessor.kt b/platform/script-debugger/backend/src/rpc/CommandProcessor.kt new file mode 100644 index 00000000..6c5cff13 --- /dev/null +++ b/platform/script-debugger/backend/src/rpc/CommandProcessor.kt @@ -0,0 +1,57 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.rpc + +import com.intellij.openapi.diagnostic.Logger +import io.netty.buffer.ByteBuf +import org.jetbrains.concurrency.createError +import org.jetbrains.jsonProtocol.Request +import java.util.concurrent.atomic.AtomicInteger + +val LOG: Logger = Logger.getInstance(CommandProcessor::class.java) + +abstract class CommandProcessor<INCOMING, INCOMING_WITH_SEQ : Any, SUCCESS_RESPONSE : Any?> : CommandSenderBase<SUCCESS_RESPONSE>(), + MessageManager.Handler<Request<*>, INCOMING, INCOMING_WITH_SEQ, SUCCESS_RESPONSE>, + ResultReader<SUCCESS_RESPONSE>, + MessageProcessor { + private val currentSequence = AtomicInteger() + @Suppress("LeakingThis") + protected val messageManager: MessageManager<Request<*>, INCOMING, INCOMING_WITH_SEQ, SUCCESS_RESPONSE> = MessageManager(this) + + override fun cancelWaitingRequests() { + messageManager.cancelWaitingRequests() + } + + override fun closed() { + messageManager.closed() + } + + override fun getUpdatedSequence(message: Request<*>): Int { + val id = currentSequence.incrementAndGet() + message.finalize(id) + return id + } + + final override fun <RESULT> doSend(message: Request<RESULT>, callback: RequestPromise<SUCCESS_RESPONSE, RESULT>) { + messageManager.send(message, callback) + } +} + +fun requestToByteBuf(message: Request<*>, isDebugEnabled: Boolean = LOG.isDebugEnabled): ByteBuf { + val content = message.buffer + if (isDebugEnabled) { + LOG.debug("OUT: ${content.toString(Charsets.UTF_8)}") + } + return content +} + +interface ResultReader<in RESPONSE> { + fun <RESULT> readResult(readMethodName: String, successResponse: RESPONSE): RESULT? +} + +interface RequestCallback<SUCCESS_RESPONSE> { + fun onSuccess(response: SUCCESS_RESPONSE?, resultReader: ResultReader<SUCCESS_RESPONSE>?) + + fun onError(error: Throwable) + + fun onError(error: String): Unit = onError(createError(error)) +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/rpc/CommandSenderBase.kt b/platform/script-debugger/backend/src/rpc/CommandSenderBase.kt new file mode 100644 index 00000000..40751ebe --- /dev/null +++ b/platform/script-debugger/backend/src/rpc/CommandSenderBase.kt @@ -0,0 +1,40 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.rpc + +import org.jetbrains.concurrency.AsyncPromise +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.catchError +import org.jetbrains.jsonProtocol.Request + +abstract class CommandSenderBase<SUCCESS_RESPONSE> { + protected abstract fun <RESULT> doSend(message: Request<RESULT>, callback: RequestPromise<SUCCESS_RESPONSE, RESULT>) + + fun <RESULT : Any?> send(message: Request<RESULT>): Promise<RESULT> { + val callback = RequestPromise<SUCCESS_RESPONSE, RESULT>(message.methodName) + doSend(message, callback) + return callback + } +} + +class RequestPromise<SUCCESS_RESPONSE, RESULT : Any?>(private val methodName: String?) : AsyncPromise<RESULT>(), RequestCallback<SUCCESS_RESPONSE> { + override fun onSuccess(response: SUCCESS_RESPONSE?, resultReader: ResultReader<SUCCESS_RESPONSE>?) { + catchError { + val r: Any? + if (resultReader == null || response == null) { + r = response + } + else if (methodName == null) { + r = null + } + else { + r = resultReader.readResult(methodName, response) + } + + UnsafeSetResult.setResult(this, r) + } + } + + override fun onError(error: Throwable) { + setError(error) + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/rpc/MessageManager.kt b/platform/script-debugger/backend/src/rpc/MessageManager.kt new file mode 100644 index 00000000..325ab331 --- /dev/null +++ b/platform/script-debugger/backend/src/rpc/MessageManager.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.rpc + +import com.intellij.util.containers.ContainerUtil +import org.jetbrains.concurrency.Promise +import org.jetbrains.jsonProtocol.Request +import java.io.IOException +import java.util.* + +interface MessageProcessor { + fun cancelWaitingRequests() + + fun closed() + + fun <RESULT> send(message: Request<RESULT>): Promise<RESULT> +} + +class MessageManager<REQUEST, INCOMING, INCOMING_WITH_SEQ : Any, SUCCESS>(private val handler: MessageManager.Handler<REQUEST, INCOMING, INCOMING_WITH_SEQ, SUCCESS>) : MessageManagerBase() { + private val callbackMap = ContainerUtil.createConcurrentIntObjectMap<RequestCallback<SUCCESS>>() + + interface Handler<OUTGOING, INCOMING, INCOMING_WITH_SEQ : Any, SUCCESS> { + fun getUpdatedSequence(message: OUTGOING): Int + + @Throws(IOException::class) + fun write(message: OUTGOING): Boolean + + fun readIfHasSequence(incoming: INCOMING): INCOMING_WITH_SEQ? + + fun getSequence(incomingWithSeq: INCOMING_WITH_SEQ): Int = throw AbstractMethodError() + + fun getSequence(incomingWithSeq: INCOMING_WITH_SEQ, incoming: INCOMING): Int = getSequence(incomingWithSeq) + + fun acceptNonSequence(incoming: INCOMING) + + fun call(response: INCOMING_WITH_SEQ, callback: RequestCallback<SUCCESS>) + } + + fun send(message: REQUEST, callback: RequestCallback<SUCCESS>) { + if (rejectIfClosed(callback)) { + return + } + + val sequence = handler.getUpdatedSequence(message) + callbackMap.put(sequence, callback) + + val success: Boolean + try { + success = handler.write(message) + } + catch (e: Throwable) { + try { + failedToSend(sequence) + } + finally { + LOG.error("Failed to send", e) + } + return + } + + if (!success) { + failedToSend(sequence) + } + } + + private fun failedToSend(sequence: Int) { + callbackMap.remove(sequence)?.onError("Failed to send") + } + + fun processIncoming(incomingParsed: INCOMING) { + val commandResponse = handler.readIfHasSequence(incomingParsed) + if (commandResponse == null) { + if (closed) { + // just ignore + LOG.info("Connection closed, ignore incoming") + } + else { + handler.acceptNonSequence(incomingParsed) + } + return + } + + val callback = getCallbackAndRemove(handler.getSequence(commandResponse, incomingParsed)) + if (rejectIfClosed(callback)) { + return + } + + try { + handler.call(commandResponse, callback) + } + catch (e: Throwable) { + callback.onError(e) + LOG.error("Failed to dispatch response to callback", e) + } + } + + fun getCallbackAndRemove(id: Int): RequestCallback<SUCCESS> = callbackMap.remove(id) ?: throw IllegalArgumentException("Cannot find callback with id $id") + + fun cancelWaitingRequests() { + // we should call them in the order they have been submitted + val map = callbackMap + val keys = map.keys() + Arrays.sort(keys) + for (key in keys) { + map.get(key)?.reject() + } + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/rpc/MessageManagerBase.kt b/platform/script-debugger/backend/src/rpc/MessageManagerBase.kt new file mode 100644 index 00000000..b92be849 --- /dev/null +++ b/platform/script-debugger/backend/src/rpc/MessageManagerBase.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.rpc + +const val CONNECTION_CLOSED_MESSAGE: String = "Connection closed" + +abstract class MessageManagerBase { + @Volatile protected var closed: Boolean = false + + protected fun rejectIfClosed(callback: RequestCallback<*>): Boolean { + if (closed) { + callback.onError("Connection closed") + return true + } + return false + } + + fun closed() { + closed = true + } +} + +fun RequestCallback<*>.reject() { + onError(CONNECTION_CLOSED_MESSAGE) +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/rpc/UnsafeSetResult.java b/platform/script-debugger/backend/src/rpc/UnsafeSetResult.java new file mode 100644 index 00000000..aeed0568 --- /dev/null +++ b/platform/script-debugger/backend/src/rpc/UnsafeSetResult.java @@ -0,0 +1,15 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.rpc; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.concurrency.AsyncPromise; + +// we cannot fix all WIP types to be nullable for now, +// but don't want to to use explicitly nullable result type for method setResult +class UnsafeSetResult { + static <T> void setResult(@NotNull AsyncPromise<T> promise, @Nullable Object result) { + //noinspection unchecked + promise.setResult((T)result); + } +} diff --git a/platform/script-debugger/debugger-ui/intellij.platform.scriptDebugger.ui.iml b/platform/script-debugger/debugger-ui/intellij.platform.scriptDebugger.ui.iml new file mode 100644 index 00000000..663770e0 --- /dev/null +++ b/platform/script-debugger/debugger-ui/intellij.platform.scriptDebugger.ui.iml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/testSrc" isTestSource="true" packagePrefix="org.jetbrains.debugger" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + <orderEntry type="module" module-name="intellij.platform.debugger" /> + <orderEntry type="module" module-name="intellij.platform.ide.impl" /> + <orderEntry type="module" module-name="intellij.platform.scriptDebugger.backend" /> + <orderEntry type="module" module-name="intellij.platform.debugger.impl" /> + <orderEntry type="library" name="Guava" level="project" /> + <orderEntry type="module" module-name="intellij.platform.lang.impl" /> + <orderEntry type="library" name="netty-codec-http" level="project" /> + </component> +</module>
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/BasicDebuggerViewSupport.kt b/platform/script-debugger/debugger-ui/src/BasicDebuggerViewSupport.kt new file mode 100644 index 00000000..d5a63deb --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/BasicDebuggerViewSupport.kt @@ -0,0 +1,41 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger + +import com.intellij.xdebugger.frame.XCompositeNode +import com.intellij.xdebugger.frame.XValueChildrenList +import com.intellij.xdebugger.frame.XValueNode +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.onError +import org.jetbrains.concurrency.onSuccess +import org.jetbrains.concurrency.resolvedPromise +import org.jetbrains.debugger.values.ObjectValue +import org.jetbrains.debugger.values.Value +import javax.swing.Icon + +open class BasicDebuggerViewSupport : MemberFilter, DebuggerViewSupport { + protected val defaultMemberFilterPromise: Promise<MemberFilter> = resolvedPromise<MemberFilter>(this) + + override fun propertyNamesToString(list: List<String>, quotedAware: Boolean): String = ValueModifierUtil.propertyNamesToString(list, quotedAware) + + override fun computeObjectPresentation(value: ObjectValue, variable: Variable, context: VariableContext, node: XValueNode, icon: Icon): Unit = VariableView.setObjectPresentation(value, icon, node) + + override fun computeArrayPresentation(value: Value, variable: Variable, context: VariableContext, node: XValueNode, icon: Icon) { + VariableView.setArrayPresentation(value, context, icon, node) + } + + override fun getMemberFilter(context: VariableContext): Promise<MemberFilter> = defaultMemberFilterPromise + + override fun computeReceiverVariable(context: VariableContext, callFrame: CallFrame, node: XCompositeNode): Promise<*> { + return callFrame.receiverVariable + .onSuccess(node) { + node.addChildren(if (it == null) XValueChildrenList.EMPTY else XValueChildrenList.singleton(VariableView(it, context)), true) + } + .onError(node) { + node.addChildren(XValueChildrenList.EMPTY, true) + } + } +} + +interface PresentationProvider { + fun computePresentation(node: XValueNode, icon: Icon): Boolean +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/CallFrameView.kt b/platform/script-debugger/debugger-ui/src/CallFrameView.kt new file mode 100644 index 00000000..69566437 --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/CallFrameView.kt @@ -0,0 +1,86 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger.frame + +import com.intellij.icons.AllIcons +import com.intellij.ui.ColoredTextContainer +import com.intellij.ui.SimpleTextAttributes +import com.intellij.xdebugger.evaluation.XDebuggerEvaluator +import com.intellij.xdebugger.frame.XCompositeNode +import com.intellij.xdebugger.frame.XStackFrame +import org.jetbrains.concurrency.Promise +import org.jetbrains.debugger.* + +// isInLibraryContent call could be costly, so we compute it only once (our customizePresentation called on each repaint) +class CallFrameView @JvmOverloads constructor(val callFrame: CallFrame, + override val viewSupport: DebuggerViewSupport, + val script: Script? = null, + sourceInfo: SourceInfo? = null, + isInLibraryContent: Boolean? = null, + override val vm: Vm? = null) : XStackFrame(), VariableContext { + private val sourceInfo = sourceInfo ?: viewSupport.getSourceInfo(script, callFrame) + private val isInLibraryContent: Boolean = isInLibraryContent ?: (this.sourceInfo != null && viewSupport.isInLibraryContent(this.sourceInfo, script)) + + private var evaluator: XDebuggerEvaluator? = null + + override fun getEqualityObject(): Any = callFrame.equalityObject + + override fun computeChildren(node: XCompositeNode) { + node.setAlreadySorted(true) + createAndAddScopeList(node, callFrame.variableScopes, this, callFrame) + } + + override val evaluateContext: EvaluateContext + get() = callFrame.evaluateContext + + override fun watchableAsEvaluationExpression(): Boolean = true + + override val memberFilter: Promise<MemberFilter> + get() = viewSupport.getMemberFilter(this) + + fun getMemberFilter(scope: Scope): Promise<MemberFilter> = createVariableContext(scope, this, callFrame).memberFilter + + override fun getEvaluator(): XDebuggerEvaluator? { + if (evaluator == null) { + evaluator = viewSupport.createFrameEvaluator(this) + } + return evaluator + } + + override fun getSourcePosition(): SourceInfo? = sourceInfo + + override fun customizePresentation(component: ColoredTextContainer) { + if (sourceInfo == null) { + val scriptName = if (script == null) "unknown" else script.url.trimParameters().toDecodedForm() + val line = callFrame.line + component.append(if (line == -1) scriptName else "$scriptName:$line", SimpleTextAttributes.ERROR_ATTRIBUTES) + return + } + + val fileName = sourceInfo.file.name + val line = sourceInfo.line + 1 + + val textAttributes = + if (isInLibraryContent || callFrame.isFromAsyncStack) SimpleTextAttributes.GRAYED_ATTRIBUTES + else SimpleTextAttributes.REGULAR_ATTRIBUTES + + val functionName = sourceInfo.functionName + if (functionName == null || (functionName.isEmpty() && callFrame.hasOnlyGlobalScope)) { + if (fileName.startsWith("index.")) { + sourceInfo.file.parent?.let { + component.append("${it.name}/", textAttributes) + } + } + component.append("$fileName:$line", textAttributes) + } + else { + if (functionName.isEmpty()) { + component.append("anonymous", if (isInLibraryContent) SimpleTextAttributes.GRAYED_ITALIC_ATTRIBUTES else SimpleTextAttributes.REGULAR_ITALIC_ATTRIBUTES) + } + else { + component.append(functionName, textAttributes) + } + component.append("(), $fileName:$line", textAttributes) + } + component.setIcon(AllIcons.Debugger.Frame) + } +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/DebugProcessImpl.kt b/platform/script-debugger/debugger-ui/src/DebugProcessImpl.kt new file mode 100644 index 00000000..3fb9e5da --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/DebugProcessImpl.kt @@ -0,0 +1,261 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger + +import com.intellij.execution.DefaultExecutionResult +import com.intellij.execution.ExecutionResult +import com.intellij.execution.process.ProcessHandler +import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.Url +import com.intellij.util.containers.ContainerUtil +import com.intellij.util.io.socketConnection.ConnectionStatus +import com.intellij.xdebugger.DefaultDebugProcessHandler +import com.intellij.xdebugger.XDebugProcess +import com.intellij.xdebugger.XDebugSession +import com.intellij.xdebugger.breakpoints.XBreakpointHandler +import com.intellij.xdebugger.breakpoints.XLineBreakpoint +import com.intellij.xdebugger.evaluation.XDebuggerEditorsProvider +import com.intellij.xdebugger.frame.XSuspendContext +import com.intellij.xdebugger.impl.XDebugSessionImpl +import com.intellij.xdebugger.stepping.XSmartStepIntoHandler +import org.jetbrains.concurrency.Promise +import org.jetbrains.debugger.connection.RemoteVmConnection +import org.jetbrains.debugger.connection.VmConnection +import java.util.concurrent.ConcurrentMap +import java.util.concurrent.atomic.AtomicBoolean +import javax.swing.event.HyperlinkListener + +interface MultiVmDebugProcess { + val mainVm: Vm? + val activeOrMainVm: Vm? + val collectVMs: List<Vm> + get() { + val mainVm = mainVm ?: return emptyList() + val result = mutableListOf<Vm>() + fun addRecursively(vm: Vm) { + if (vm.attachStateManager.isAttached) { + result.add(vm) + vm.childVMs.forEach { addRecursively(it) } + } + } + addRecursively(mainVm) + return result + } +} + +abstract class DebugProcessImpl<out C : VmConnection<*>>(session: XDebugSession, + val connection: C, + private val editorsProvider: XDebuggerEditorsProvider, + private val smartStepIntoHandler: XSmartStepIntoHandler<*>? = null, + protected val executionResult: ExecutionResult? = null) : XDebugProcess(session), MultiVmDebugProcess { + protected val repeatStepInto: AtomicBoolean = AtomicBoolean() + @Volatile var lastStep: StepAction? = null + @Volatile protected var lastCallFrame: CallFrame? = null + @Volatile protected var isForceStep: Boolean = false + @Volatile protected var disableDoNotStepIntoLibraries: Boolean = false + + protected val urlToFileCache: ConcurrentMap<Url, VirtualFile> = ContainerUtil.newConcurrentMap<Url, VirtualFile>() + + var processBreakpointConditionsAtIdeSide: Boolean = false + + private val connectedListenerAdded = AtomicBoolean() + private val breakpointsInitiated = AtomicBoolean() + + private val _breakpointHandlers: Array<XBreakpointHandler<*>> by lazy(LazyThreadSafetyMode.NONE) { createBreakpointHandlers() } + + protected val realProcessHandler: ProcessHandler? + get() = executionResult?.processHandler + + final override fun getSmartStepIntoHandler(): XSmartStepIntoHandler<*>? = smartStepIntoHandler + + final override fun getBreakpointHandlers(): Array<out XBreakpointHandler<*>> = when (connection.state.status) { + ConnectionStatus.DISCONNECTED, ConnectionStatus.DETACHED, ConnectionStatus.CONNECTION_FAILED -> XBreakpointHandler.EMPTY_ARRAY + else -> _breakpointHandlers + } + + final override fun getEditorsProvider(): XDebuggerEditorsProvider = editorsProvider + + val vm: Vm? + get() = connection.vm + + final override val mainVm: Vm? + get() = connection.vm + + final override val activeOrMainVm: Vm? + get() = (session.suspendContext?.activeExecutionStack as? ExecutionStackView)?.suspendContext?.vm ?: mainVm + + init { + if (session is XDebugSessionImpl && executionResult is DefaultExecutionResult) { + session.addRestartActions(*executionResult.restartActions) + } + connection.stateChanged { + when (it.status) { + ConnectionStatus.DISCONNECTED, ConnectionStatus.DETACHED -> { + if (it.status == ConnectionStatus.DETACHED) { + if (realProcessHandler != null) { + // here must we must use effective process handler + processHandler.detachProcess() + } + } + getSession().stop() + } + + ConnectionStatus.CONNECTION_FAILED -> { + getSession().reportError(it.message) + getSession().stop() + } + + else -> getSession().rebuildViews() + } + } + } + + protected abstract fun createBreakpointHandlers(): Array<XBreakpointHandler<*>> + + private fun updateLastCallFrame(vm: Vm) { + lastCallFrame = vm.suspendContextManager.context?.topFrame + } + + final override fun checkCanPerformCommands(): Boolean = activeOrMainVm != null + + final override fun isValuesCustomSorted(): Boolean = true + + final override fun startStepOver(context: XSuspendContext?) { + val vm = context.vm + updateLastCallFrame(vm) + continueVm(vm, StepAction.OVER) + } + + val XSuspendContext?.vm: Vm + get() = (this as? SuspendContextView)?.activeVm ?: mainVm!! + + final override fun startForceStepInto(context: XSuspendContext?) { + isForceStep = true + startStepInto(context) + } + + final override fun startStepInto(context: XSuspendContext?) { + val vm = context.vm + updateLastCallFrame(vm) + continueVm(vm, StepAction.IN) + } + + final override fun startStepOut(context: XSuspendContext?) { + val vm = context.vm + if (isVmStepOutCorrect()) { + lastCallFrame = null + } + else { + updateLastCallFrame(vm) + } + continueVm(vm, StepAction.OUT) + } + + // some VM (firefox for example) doesn't implement step out correctly, so, we need to fix it + protected open fun isVmStepOutCorrect(): Boolean = true + + override fun resume(context: XSuspendContext?) { + continueVm(context.vm, StepAction.CONTINUE) + } + + open fun resume(vm: Vm) { + continueVm(vm, StepAction.CONTINUE) + } + + @Suppress("unused") + @Deprecated("Pass vm explicitly", ReplaceWith("continueVm(vm!!, stepAction)")) + protected open fun continueVm(stepAction: StepAction): Promise<*>? = continueVm(activeOrMainVm!!, stepAction) + + /** + * You can override this method to avoid SuspendContextManager implementation, but it is not recommended. + */ + protected open fun continueVm(vm: Vm, stepAction: StepAction): Promise<*>? { + val suspendContextManager = vm.suspendContextManager + if (stepAction === StepAction.CONTINUE) { + if (suspendContextManager.context == null) { + // on resumed we ask session to resume, and session then call our "resume", but we have already resumed, so, we don't need to send "continue" message + return null + } + + lastStep = null + lastCallFrame = null + urlToFileCache.clear() + disableDoNotStepIntoLibraries = false + } + else { + lastStep = stepAction + } + return suspendContextManager.continueVm(stepAction) + } + + protected fun setOverlay(context: SuspendContext<*>) { + val vm = mainVm + if (context.vm == vm) { + vm.suspendContextManager.setOverlayMessage("Paused in debugger") + } + } + + final override fun startPausing() { + activeOrMainVm!!.suspendContextManager.suspend() + .onError(RejectErrorReporter(session, "Cannot pause")) + } + + final override fun getCurrentStateMessage(): String = connection.state.message + + final override fun getCurrentStateHyperlinkListener(): HyperlinkListener? = connection.state.messageLinkListener + + override fun doGetProcessHandler(): ProcessHandler = executionResult?.processHandler ?: object : DefaultDebugProcessHandler() { override fun isSilentlyDestroyOnClose() = true } + + fun saveResolvedFile(url: Url, file: VirtualFile) { + urlToFileCache.putIfAbsent(url, file) + } + + // go plugin compatibility + @Suppress("unused") + open fun getLocationsForBreakpoint(breakpoint: XLineBreakpoint<*>): List<Location> = getLocationsForBreakpoint(activeOrMainVm!!, breakpoint) + + open fun getLocationsForBreakpoint(vm: Vm, breakpoint: XLineBreakpoint<*>): List<Location> = throw UnsupportedOperationException() + + override fun isLibraryFrameFilterSupported(): Boolean = true + + // todo make final (go plugin compatibility) + override fun checkCanInitBreakpoints(): Boolean { + if (connection.state.status == ConnectionStatus.CONNECTED) { + return true + } + + if (connectedListenerAdded.compareAndSet(false, true)) { + connection.stateChanged { + if (it.status == ConnectionStatus.CONNECTED) { + initBreakpoints() + } + } + } + return false + } + + protected fun initBreakpoints() { + if (breakpointsInitiated.compareAndSet(false, true)) { + doInitBreakpoints() + } + } + + protected open fun doInitBreakpoints() { + mainVm?.let(::beforeInitBreakpoints) + runReadAction { session.initBreakpoints() } + } + + protected open fun beforeInitBreakpoints(vm: Vm) { + } + + protected fun addChildVm(vm: Vm, childConnection: RemoteVmConnection<*>) { + mainVm?.childVMs?.add(vm) + childConnection.stateChanged { + if (it.status == ConnectionStatus.CONNECTION_FAILED || it.status == ConnectionStatus.DISCONNECTED || it.status == ConnectionStatus.DETACHED) { + mainVm?.childVMs?.remove(vm) + } + } + + mainVm?.debugListener?.childVmAdded(vm) + } +} diff --git a/platform/script-debugger/debugger-ui/src/DebuggerViewSupport.kt b/platform/script-debugger/debugger-ui/src/DebuggerViewSupport.kt new file mode 100644 index 00000000..93fbc2d8 --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/DebuggerViewSupport.kt @@ -0,0 +1,74 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger + +import com.intellij.util.ThreeState +import com.intellij.xdebugger.XSourcePosition +import com.intellij.xdebugger.evaluation.XDebuggerEvaluator +import com.intellij.xdebugger.frame.XCompositeNode +import com.intellij.xdebugger.frame.XInlineDebuggerDataCallback +import com.intellij.xdebugger.frame.XNavigatable +import com.intellij.xdebugger.frame.XValueNode +import org.jetbrains.concurrency.Promise +import org.jetbrains.debugger.frame.CallFrameView +import org.jetbrains.debugger.values.ObjectValue +import org.jetbrains.debugger.values.Value +import org.jetbrains.rpc.LOG +import javax.swing.Icon + +interface DebuggerViewSupport { + val vm: Vm? + get() = null + + fun getSourceInfo(script: Script?, frame: CallFrame): SourceInfo? = null + + fun getSourceInfo(functionName: String?, scriptUrl: String, line: Int, column: Int): SourceInfo? = null + + fun getSourceInfo(functionName: String?, script: Script, line: Int, column: Int): SourceInfo? = null + + fun propertyNamesToString(list: List<String>, quotedAware: Boolean): String + + // Please, don't hesitate to ask to share some generic implementations. Don't reinvent the wheel and keep in mind - user expects the same UI across all IDEA-based IDEs. + fun computeObjectPresentation(value: ObjectValue, variable: Variable, context: VariableContext, node: XValueNode, icon: Icon) + + fun computeArrayPresentation(value: Value, variable: Variable, context: VariableContext, node: XValueNode, icon: Icon) + + fun createFrameEvaluator(frame: CallFrameView): XDebuggerEvaluator = PromiseDebuggerEvaluator(frame) + + /** + * [org.jetbrains.debugger.values.FunctionValue] is special case and handled by SDK + */ + fun canNavigateToSource(variable: Variable, context: VariableContext): Boolean = false + + fun computeSourcePosition(name: String, value: Value?, variable: Variable, context: VariableContext, navigatable: XNavigatable) { + } + + fun computeInlineDebuggerData(name: String, variable: Variable, context: VariableContext, callback: XInlineDebuggerDataCallback): ThreeState = ThreeState.UNSURE + + // return null if you don't need to add additional properties + fun computeAdditionalObjectProperties(value: ObjectValue, variable: Variable, context: VariableContext, node: XCompositeNode): Promise<Any?>? = null + + fun getMemberFilter(context: VariableContext): Promise<MemberFilter> + + fun transformErrorOnGetUsedReferenceValue(value: Value?, error: String?): Value? = value + + fun isInLibraryContent(sourceInfo: SourceInfo, script: Script?): Boolean = false + + fun computeReceiverVariable(context: VariableContext, callFrame: CallFrame, node: XCompositeNode): Promise<*> +} + +open class PromiseDebuggerEvaluator(protected val context: VariableContext) : XDebuggerEvaluator() { + final override fun evaluate(expression: String, callback: XDebuggerEvaluator.XEvaluationCallback, expressionPosition: XSourcePosition?) { + try { + evaluate(expression, expressionPosition) + .onSuccess { callback.evaluated(VariableView(VariableImpl(expression, it.value), context)) } + .onError { callback.errorOccurred(it.message ?: it.toString()) } + } + catch (e: Throwable) { + LOG.error(e) + callback.errorOccurred(e.toString()) + return + } + } + + protected open fun evaluate(expression: String, expressionPosition: XSourcePosition?): Promise<EvaluateResult> = context.evaluateContext.evaluate(expression) +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/FunctionScopesValueGroup.kt b/platform/script-debugger/debugger-ui/src/FunctionScopesValueGroup.kt new file mode 100644 index 00000000..28016637 --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/FunctionScopesValueGroup.kt @@ -0,0 +1,32 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger + +import com.intellij.xdebugger.frame.XCompositeNode +import com.intellij.xdebugger.frame.XValueChildrenList +import com.intellij.xdebugger.frame.XValueGroup +import org.jetbrains.concurrency.errorIfNotMessage +import org.jetbrains.concurrency.onSuccess +import org.jetbrains.debugger.values.FunctionValue +import org.jetbrains.rpc.LOG +import java.util.* + +internal class FunctionScopesValueGroup(private val functionValue: FunctionValue, private val variableContext: VariableContext) : XValueGroup("Function scopes") { + override fun computeChildren(node: XCompositeNode) { + node.setAlreadySorted(true) + + functionValue.resolve() + .onSuccess(node) { + val scopes = it.scopes + if (scopes == null || scopes.size == 0) { + node.addChildren(XValueChildrenList.EMPTY, true) + } + else { + createAndAddScopeList(node, Arrays.asList(*scopes), variableContext, null) + } + } + .onError { + LOG.errorIfNotMessage(it) + node.setErrorMessage(it.message!!) + } + } +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/MemberFilter.kt b/platform/script-debugger/debugger-ui/src/MemberFilter.kt new file mode 100644 index 00000000..dda5f961 --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/MemberFilter.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +interface MemberFilter { + fun isMemberVisible(variable: Variable): Boolean = variable.isReadable + + val additionalVariables: Collection<Variable> + get() = emptyList() + + fun rawNameToSource(variable: Variable): String = variable.name + + fun sourceNameToRaw(name: String): String? = null + + fun hasNameMappings(): Boolean = false +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/ProcessHandlerWrapper.kt b/platform/script-debugger/debugger-ui/src/ProcessHandlerWrapper.kt new file mode 100644 index 00000000..9d2998ba --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/ProcessHandlerWrapper.kt @@ -0,0 +1,76 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger + +import com.intellij.execution.KillableProcess +import com.intellij.execution.process.ProcessAdapter +import com.intellij.execution.process.ProcessEvent +import com.intellij.execution.process.ProcessHandler +import com.intellij.xdebugger.XDebugProcess +import org.jetbrains.rpc.LOG +import java.io.OutputStream + +class ProcessHandlerWrapper(private val debugProcess: XDebugProcess, private val handler: ProcessHandler) : ProcessHandler(), KillableProcess { + init { + if (handler.isStartNotified) { + super.startNotify() + } + + handler.addProcessListener(object : ProcessAdapter() { + override fun startNotified(event: ProcessEvent) { + super@ProcessHandlerWrapper.startNotify() + } + + override fun processTerminated(event: ProcessEvent) { + notifyProcessTerminated(event.exitCode) + } + }) + } + + override fun isSilentlyDestroyOnClose(): Boolean = handler.isSilentlyDestroyOnClose + + override fun startNotify() { + handler.startNotify() + } + + override fun destroyProcessImpl() { + stop(true) + } + + override fun detachProcessImpl() { + stop(false) + } + + private fun stop(destroy: Boolean) { + fun stopProcess(destroy: Boolean) { + if (destroy) { + handler.destroyProcess() + } + else { + handler.detachProcess() + } + } + + debugProcess.stopAsync() + .onSuccess() { stopProcess(destroy) } + .onError { + try { + LOG.error(it) + } + finally { + stopProcess(destroy) + } + } + } + + override fun detachIsDefault(): Boolean = handler.detachIsDefault() + + override fun getProcessInput(): OutputStream? = handler.processInput + + override fun canKillProcess(): Boolean = handler is KillableProcess && handler.canKillProcess() + + override fun killProcess() { + if (handler is KillableProcess) { + handler.killProcess() + } + } +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/RemoteVmConnection.kt b/platform/script-debugger/debugger-ui/src/RemoteVmConnection.kt new file mode 100644 index 00000000..2222566b --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/RemoteVmConnection.kt @@ -0,0 +1,171 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger.connection + +import com.intellij.execution.ExecutionException +import com.intellij.execution.process.ProcessHandler +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.util.Condition +import com.intellij.openapi.util.Conditions +import com.intellij.ui.ColoredListCellRenderer +import com.intellij.util.containers.ContainerUtil +import com.intellij.util.io.connectRetrying +import com.intellij.util.io.socketConnection.ConnectionStatus +import io.netty.bootstrap.Bootstrap +import org.jetbrains.concurrency.* +import org.jetbrains.debugger.Vm +import org.jetbrains.io.NettyUtil +import org.jetbrains.rpc.LOG +import java.net.ConnectException +import java.net.InetAddress +import java.net.InetSocketAddress +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference +import java.util.function.Consumer +import javax.swing.JList + +abstract class RemoteVmConnection<VmT : Vm> : VmConnection<VmT>() { + + var address: InetSocketAddress? = null + + private val connectCancelHandler = AtomicReference<() -> Unit>() + + abstract fun createBootstrap(address: InetSocketAddress, vmResult: AsyncPromise<VmT>): Bootstrap + + @JvmOverloads + fun open(address: InetSocketAddress, stopCondition: Condition<Void>? = null): Promise<VmT> { + if (address.isUnresolved) { + val error = "Host ${address.hostString} is unresolved" + setState(ConnectionStatus.CONNECTION_FAILED, error) + return rejectedPromise(error) + } + + this.address = address + setState(ConnectionStatus.WAITING_FOR_CONNECTION, "Connecting to ${address.hostString}:${address.port}") + + val result = AsyncPromise<VmT>() + result + .onSuccess { + connectionSucceeded(it, address) + } + .onError { + if (it !is ConnectException) { + LOG.errorIfNotMessage(it) + } + setState(ConnectionStatus.CONNECTION_FAILED, it.message) + } + .onProcessed { + connectCancelHandler.set(null) + } + + val future = ApplicationManager.getApplication().executeOnPooledThread { + if (Thread.interrupted()) { + return@executeOnPooledThread + } + connectCancelHandler.set { result.setError("Closed explicitly") } + + doOpen(result, address, stopCondition) + } + + connectCancelHandler.set { + try { + future.cancel(true) + } + finally { + result.setError("Cancelled") + } + } + return result + } + + protected fun connectionSucceeded(it: VmT, address: InetSocketAddress) { + vm = it + setState(ConnectionStatus.CONNECTED, "Connected to ${connectedAddressToPresentation(address, it)}") + startProcessing() + } + + protected open fun doOpen(result: AsyncPromise<VmT>, address: InetSocketAddress, stopCondition: Condition<Void>?) { + val maxAttemptCount = if (stopCondition == null) NettyUtil.DEFAULT_CONNECT_ATTEMPT_COUNT else -1 + val resultRejected = Condition<Void> { result.state == Promise.State.REJECTED } + val combinedCondition = Conditions.or(stopCondition ?: Conditions.alwaysFalse(), resultRejected) + val connectResult = createBootstrap(address, result).connectRetrying(address, maxAttemptCount, combinedCondition) + connectResult.handleError(Consumer { result.setError(it) }) + connectResult.handleThrowable(Consumer { result.setError(it) }) + val channel = connectResult.channel + channel?.closeFuture()?.addListener { + if (result.isSucceeded) { + close("Process disconnected unexpectedly", ConnectionStatus.DISCONNECTED) + } + } + if (channel != null) { + stateChanged { + if (it.status == ConnectionStatus.DISCONNECTED) { + channel.close() + } + } + } + } + + protected open fun connectedAddressToPresentation(address: InetSocketAddress, vm: Vm): String = "${address.hostName}:${address.port}" + + override fun detachAndClose(): Promise<*> { + try { + connectCancelHandler.getAndSet(null)?.invoke() + } + finally { + return super.detachAndClose() + } + } +} + +fun RemoteVmConnection<*>.open(address: InetSocketAddress, processHandler: ProcessHandler): Promise<out Vm> = open(address, Condition<java.lang.Void> { processHandler.isProcessTerminating || processHandler.isProcessTerminated }) + +fun <T> chooseDebuggee(targets: Collection<T>, selectedIndex: Int, renderer: (T, ColoredListCellRenderer<*>) -> Unit): Promise<T> { + if (targets.size == 1) { + return resolvedPromise(targets.first()) + } + else if (targets.isEmpty()) { + return rejectedPromise("No tabs to inspect") + } + + val result = org.jetbrains.concurrency.AsyncPromise<T>() + ApplicationManager.getApplication().invokeLater { + val model = ContainerUtil.newArrayList(targets) + val builder = JBPopupFactory.getInstance() + .createPopupChooserBuilder(model) + .setRenderer( + object : ColoredListCellRenderer<T>() { + override fun customizeCellRenderer(list: JList<out T>, value: T, index: Int, selected: Boolean, hasFocus: Boolean) { + renderer(value, this) + } + }) + .setTitle("Choose Page to Debug") + .setCancelOnWindowDeactivation(false) + .setItemChosenCallback { value -> + result.setResult(value) + } + if (selectedIndex != -1) { + builder.setSelectedValue(model[selectedIndex], false) + } + builder + .createPopup() + .showInFocusCenter() + } + return result +} + +@Deprecated("Use NodeCommandLineUtil.initRemoteVmConnectionSync instead") +@Throws(ExecutionException::class) +fun initRemoteVmConnectionSync(connection: RemoteVmConnection<*>, debugPort: Int): Vm { + val address = InetSocketAddress(InetAddress.getLoopbackAddress(), debugPort) + val vmPromise = connection.open(address) + val vm: Vm + try { + vm = vmPromise.blockingGet(30, TimeUnit.SECONDS)!! + } + catch (e: Exception) { + throw ExecutionException("Cannot connect to VM ($address)", e) + } + + return vm +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/ScopeVariablesGroup.kt b/platform/script-debugger/debugger-ui/src/ScopeVariablesGroup.kt new file mode 100644 index 00000000..9c1c0cdc --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/ScopeVariablesGroup.kt @@ -0,0 +1,91 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger + +import com.intellij.xdebugger.XDebuggerBundle +import com.intellij.xdebugger.frame.XCompositeNode +import com.intellij.xdebugger.frame.XValueChildrenList +import com.intellij.xdebugger.frame.XValueGroup +import org.jetbrains.concurrency.onError +import org.jetbrains.concurrency.onSuccess +import org.jetbrains.concurrency.thenAsyncAccept + +class ScopeVariablesGroup(val scope: Scope, parentContext: VariableContext, callFrame: CallFrame?) : XValueGroup(scope.createScopeNodeName()) { + private val context = createVariableContext(scope, parentContext, callFrame) + + private val callFrame = if (scope.type == ScopeType.LOCAL) callFrame else null + + override fun isAutoExpand(): Boolean = scope.type == ScopeType.BLOCK || scope.type == ScopeType.LOCAL || scope.type == ScopeType.CATCH + + override fun getComment(): String? { + val className = scope.description + return if ("Object" == className) null else className + } + + override fun computeChildren(node: XCompositeNode) { + val promise = processScopeVariables(scope, node, context, callFrame == null) + if (callFrame == null) { + return + } + + promise + .onSuccess(node) { + context.memberFilter + .thenAsyncAccept(node) { + if (it.hasNameMappings()) { + it.sourceNameToRaw(RECEIVER_NAME)?.let { + return@thenAsyncAccept callFrame.evaluateContext.evaluate(it) + .onSuccess(node) { + VariableImpl(RECEIVER_NAME, it.value, null) + node.addChildren(XValueChildrenList.singleton(VariableView( + VariableImpl(RECEIVER_NAME, it.value, null), context)), true) + } + } + } + + context.viewSupport.computeReceiverVariable(context, callFrame, node) + } + .onError(node) { + context.viewSupport.computeReceiverVariable(context, callFrame, node) + } + } + } +} + +fun createAndAddScopeList(node: XCompositeNode, scopes: List<Scope>, context: VariableContext, callFrame: CallFrame?) { + val list = XValueChildrenList(scopes.size) + for (scope in scopes) { + list.addTopGroup(ScopeVariablesGroup(scope, context, callFrame)) + } + node.addChildren(list, true) +} + +fun createVariableContext(scope: Scope, parentContext: VariableContext, callFrame: CallFrame?): VariableContext { + if (callFrame == null || scope.type == ScopeType.LIBRARY) { + // functions scopes - we can watch variables only from global scope + return ParentlessVariableContext(parentContext, scope, scope.type == ScopeType.GLOBAL) + } + else { + return VariableContextWrapper(parentContext, scope) + } +} + +private class ParentlessVariableContext(parentContext: VariableContext, scope: Scope, private val watchableAsEvaluationExpression: Boolean) : VariableContextWrapper(parentContext, scope) { + override fun watchableAsEvaluationExpression() = watchableAsEvaluationExpression +} + +private fun Scope.createScopeNodeName(): String { + when (type) { + ScopeType.GLOBAL -> return XDebuggerBundle.message("scope.global") + ScopeType.LOCAL -> return XDebuggerBundle.message("scope.local") + ScopeType.WITH -> return XDebuggerBundle.message("scope.with") + ScopeType.CLOSURE -> return XDebuggerBundle.message("scope.closure") + ScopeType.CATCH -> return XDebuggerBundle.message("scope.catch") + ScopeType.LIBRARY -> return XDebuggerBundle.message("scope.library") + ScopeType.INSTANCE -> return XDebuggerBundle.message("scope.instance") + ScopeType.CLASS -> return XDebuggerBundle.message("scope.class") + ScopeType.BLOCK -> return XDebuggerBundle.message("scope.block") + ScopeType.SCRIPT -> return XDebuggerBundle.message("scope.script") + ScopeType.UNKNOWN -> return XDebuggerBundle.message("scope.unknown") + else -> throw IllegalArgumentException(type.name) + } +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/SourceInfo.kt b/platform/script-debugger/debugger-ui/src/SourceInfo.kt new file mode 100644 index 00000000..823ff20c --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/SourceInfo.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.fileEditor.OpenFileDescriptor +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.Url +import com.intellij.util.Urls +import com.intellij.xdebugger.XSourcePosition + +class SourceInfo @JvmOverloads constructor(private val file: VirtualFile, private val line: Int, val column: Int = -1, private var offset: Int = -1, val functionName: String? = null, url: Url? = null) : XSourcePosition { + private var _url = url + + override fun getFile(): VirtualFile = file + + val url: Url + get() { + var result = _url + if (result == null) { + result = Urls.newFromVirtualFile(file) + _url = result + } + return result + } + + override fun getLine(): Int = line + + override fun getOffset(): Int { + if (offset == -1) { + val document = runReadAction { if (file.isValid) FileDocumentManager.getInstance().getDocument(file) else null } ?: return -1 + offset = if (line < document.lineCount) document.getLineStartOffset(line) else -1 + } + return offset + } + + override fun createNavigatable(project: Project): OpenFileDescriptor = OpenFileDescriptor(project, file, line, column) + + override fun toString(): String = file.path + ":" + line + if (column == -1) "" else ":" + column +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/SuspendContextView.kt b/platform/script-debugger/debugger-ui/src/SuspendContextView.kt new file mode 100644 index 00000000..274320b4 --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/SuspendContextView.kt @@ -0,0 +1,237 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger + +import com.intellij.icons.AllIcons +import com.intellij.openapi.diagnostic.logger +import com.intellij.ui.ColoredTextContainer +import com.intellij.ui.SimpleTextAttributes +import com.intellij.util.ui.UIUtil +import com.intellij.xdebugger.frame.XExecutionStack +import com.intellij.xdebugger.frame.XStackFrame +import com.intellij.xdebugger.frame.XSuspendContext +import com.intellij.xdebugger.impl.frame.XDebuggerFramesList +import com.intellij.xdebugger.settings.XDebuggerSettingsManager +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.rejectedPromise +import org.jetbrains.concurrency.resolvedPromise +import org.jetbrains.debugger.frame.CallFrameView +import org.jetbrains.debugger.values.StringValue +import java.awt.Color +import java.util.* + +/** + * Debugging several VMs simultaneously should be similar to debugging multi-threaded Java application when breakpoints suspend only one thread. + * 1. When thread is paused and another thread reaches breakpoint, show notification about it with possibility to switch thread. + * 2. Stepping and releasing affects only current thread. + * 3. When another thread is selected in Frames view, it changes its icon from (0) to (v) and becomes current, i.e. step/release commands + * are applied to it. + * 4. Stepping/releasing updates current thread icon and clears frame, but doesn't switch thread. To release other threads, user needs to + * select them firstly. + */ +abstract class SuspendContextView(protected val debugProcess: MultiVmDebugProcess, + activeStack: ExecutionStackView, + @Volatile var activeVm: Vm) + : XSuspendContext() { + + private val stacks: MutableMap<Vm, ScriptExecutionStack> = Collections.synchronizedMap(LinkedHashMap<Vm, ScriptExecutionStack>()) + + init { + val mainVm = debugProcess.mainVm + val vmList = debugProcess.collectVMs + if (mainVm != null && !vmList.isEmpty()) { + // main vm should go first + vmList.forEach { + val context = it.suspendContextManager.context + + val stack: ScriptExecutionStack = + if (context == null) { + RunningThreadExecutionStackView(it) + } + else if (context == activeStack.suspendContext) { + activeStack + } + else { + logger<SuspendContextView>().error("Paused VM was lost.") + InactiveAtBreakpointExecutionStackView(it) + } + stacks[it] = stack + } + } + else { + stacks[activeVm] = activeStack + } + } + + override fun getActiveExecutionStack(): ScriptExecutionStack? = stacks[activeVm] + + override fun getExecutionStacks(): Array<out XExecutionStack> = stacks.values.toTypedArray() + + fun evaluateExpression(expression: String): Promise<String> { + val activeStack = stacks[activeVm]!! + val frame = activeStack.topFrame ?: return rejectedPromise("Top frame is null") + if (frame !is CallFrameView) return rejectedPromise("Can't evaluate on non-paused thread") + return evaluateExpression(frame.callFrame.evaluateContext, expression) + } + + private fun evaluateExpression(evaluateContext: EvaluateContext, expression: String) = evaluateContext.evaluate(expression) + .thenAsync { + val value = it.value + if (value is StringValue && value.isTruncated) { + value.fullString + } + else { + resolvedPromise(value.valueString!!) + } + } + + fun pauseInactiveThread(inactiveThread: ExecutionStackView) { + stacks[inactiveThread.vm] = inactiveThread + } + + fun hasPausedThreads(): Boolean { + return stacks.values.any { it is ExecutionStackView } + } + + fun resume(vm: Vm) { + val prevStack = stacks[vm] + if (prevStack is ExecutionStackView) { + stacks[vm] = RunningThreadExecutionStackView(prevStack.vm) + } + } + + fun resumeCurrentThread() { + resume(activeVm) + } + + fun setActiveThread(selectedStackFrame: XStackFrame?): Boolean { + if (selectedStackFrame !is CallFrameView) return false + + var selectedVm: Vm? = null + for ((key, value) in stacks) { + if (value is ExecutionStackView && value.topFrame?.vm == selectedStackFrame.vm) { + selectedVm = key + break + } + } + + val selectedVmStack = stacks[selectedVm] + if (selectedVm != null && selectedVmStack is ExecutionStackView) { + activeVm = selectedVm + stacks[selectedVm] = selectedVmStack.copyWithIsCurrent(true) + + stacks.keys.forEach { + val stack = stacks[it] + if (it != selectedVm && stack is ExecutionStackView) { + stacks[it] = stack.copyWithIsCurrent(false) + } + } + + return stacks[selectedVm] !== selectedVmStack + } + + return false + } +} + +class RunningThreadExecutionStackView(vm: Vm) : ScriptExecutionStack(vm, vm.presentableName, AllIcons.Debugger.ThreadRunning) { + override fun computeStackFrames(firstFrameIndex: Int, container: XStackFrameContainer?) { + // add dependency to DebuggerBundle? + container?.errorOccurred("Frames not available for unsuspended thread") + } + + override fun getTopFrame(): XStackFrame? = null +} + +class InactiveAtBreakpointExecutionStackView(vm: Vm) : ScriptExecutionStack(vm, vm.presentableName, AllIcons.Debugger.ThreadAtBreakpoint) { + override fun getTopFrame(): XStackFrame? = null + + override fun computeStackFrames(firstFrameIndex: Int, container: XStackFrameContainer?) {} +} + +abstract class ScriptExecutionStack(val vm: Vm, displayName: String, icon: javax.swing.Icon): XExecutionStack(displayName, icon) { + override fun hashCode(): Int { + return vm.hashCode() + } + + override fun equals(other: Any?): Boolean { + return other is ScriptExecutionStack && other.vm == vm + } +} + +// TODO should be AllIcons.Debugger.ThreadCurrent, but because of strange logic to add non-equal XExecutionStacks we can't update icon. +private fun getThreadIcon(isCurrent: Boolean) = AllIcons.Debugger.ThreadAtBreakpoint + +class ExecutionStackView(val suspendContext: SuspendContext<*>, + internal val viewSupport: DebuggerViewSupport, + private val topFrameScript: Script?, + private val topFrameSourceInfo: SourceInfo? = null, + displayName: String = "", + isCurrent: Boolean = true) + : ScriptExecutionStack(suspendContext.vm, displayName, getThreadIcon(isCurrent)) { + + private var topCallFrameView: CallFrameView? = null + + override fun getTopFrame(): CallFrameView? { + val topCallFrame = suspendContext.topFrame + if (topCallFrameView == null || topCallFrameView!!.callFrame != topCallFrame) { + topCallFrameView = topCallFrame?.let { CallFrameView(it, viewSupport, topFrameScript, topFrameSourceInfo, vm = suspendContext.vm) } + } + return topCallFrameView + } + + override fun computeStackFrames(firstFrameIndex: Int, container: XExecutionStack.XStackFrameContainer) { + // WipSuspendContextManager set context to null on resume _before_ vm.getDebugListener().resumed() call() (in any case, XFramesView can queue event to EDT), so, IDE state could be outdated compare to VM (our) state + suspendContext.frames + .onSuccess(suspendContext) { frames -> + val count = frames.size - firstFrameIndex + val result: List<XStackFrame> + if (count < 1) { + result = emptyList() + } + else { + result = ArrayList(count) + for (i in firstFrameIndex until frames.size) { + if (i == 0) { + result.add(topFrame!!) + continue + } + + val frame = frames[i] + val asyncFunctionName = frame.asyncFunctionName + if (asyncFunctionName != null) { + result.add(AsyncFramesHeader(asyncFunctionName)) + } + // if script is null, it is native function (Object.forEach for example), so, skip it + val script = suspendContext.vm.scriptManager.getScript(frame) + if (script != null) { + val sourceInfo = viewSupport.getSourceInfo(script, frame) + val isInLibraryContent = sourceInfo != null && viewSupport.isInLibraryContent(sourceInfo, script) + if (isInLibraryContent && !XDebuggerSettingsManager.getInstance().dataViewSettings.isShowLibraryStackFrames) { + continue + } + + result.add(CallFrameView(frame, viewSupport, script, sourceInfo, isInLibraryContent, suspendContext.vm)) + } + } + } + container.addStackFrames(result, true) + } + } + + fun copyWithIsCurrent(isCurrent: Boolean): ExecutionStackView { + if (icon == getThreadIcon(isCurrent)) return this + + return ExecutionStackView(suspendContext, viewSupport, topFrameScript, topFrameSourceInfo, displayName, isCurrent) + } +} + +private val ASYNC_HEADER_ATTRIBUTES = SimpleTextAttributes(SimpleTextAttributes.STYLE_UNDERLINE or SimpleTextAttributes.STYLE_BOLD, + UIUtil.getInactiveTextColor()) + +private class AsyncFramesHeader(val asyncFunctionName: String) : XStackFrame(), XDebuggerFramesList.ItemWithCustomBackgroundColor { + override fun customizePresentation(component: ColoredTextContainer) { + component.append("Async call from $asyncFunctionName", ASYNC_HEADER_ATTRIBUTES) + } + + override fun getBackgroundColor(): Color? = null +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/VariableContext.kt b/platform/script-debugger/debugger-ui/src/VariableContext.kt new file mode 100644 index 00000000..a06dfb46 --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/VariableContext.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import org.jetbrains.concurrency.Promise + +interface VariableContext { + val evaluateContext: EvaluateContext + + /** + * Parent variable name if this context is [org.jetbrains.debugger.VariableView] + */ + val variableName: String? + get() = null + + val parent: VariableContext? + get() = null + + fun watchableAsEvaluationExpression(): Boolean + + val viewSupport: DebuggerViewSupport + + val memberFilter: Promise<MemberFilter> + get() = viewSupport.getMemberFilter(this) + + val scope: Scope? + get() = null + + val vm: Vm? + get() = null +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/VariableContextWrapper.kt b/platform/script-debugger/debugger-ui/src/VariableContextWrapper.kt new file mode 100644 index 00000000..6f0f434e --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/VariableContextWrapper.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.openapi.util.AtomicNotNullLazyValue +import org.jetbrains.concurrency.Promise + +internal open class VariableContextWrapper(override val parent: VariableContext, override val scope: Scope?) : VariableContext { + // it's worth to cache it (JavaScriptDebuggerViewSupport, for example, performs expensive computation) + private val memberFilterPromise = object : AtomicNotNullLazyValue<Promise<MemberFilter>>() { + override fun compute() = parent.viewSupport.getMemberFilter(this@VariableContextWrapper) + } + + override val variableName: String? + get() = parent.variableName + + override val memberFilter: Promise<MemberFilter> + get() = memberFilterPromise.value + + override val evaluateContext: EvaluateContext + get() = parent.evaluateContext + + override val viewSupport: DebuggerViewSupport + get() = parent.viewSupport + + override val vm: Vm? + get() = parent.vm + + override fun watchableAsEvaluationExpression() = parent.watchableAsEvaluationExpression() +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/VariableView.kt b/platform/script-debugger/debugger-ui/src/VariableView.kt new file mode 100644 index 00000000..2d60f1fe --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/VariableView.kt @@ -0,0 +1,491 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger + +import com.intellij.icons.AllIcons +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.text.StringUtil +import com.intellij.pom.Navigatable +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiReference +import com.intellij.util.SmartList +import com.intellij.util.ThreeState +import com.intellij.xdebugger.XExpression +import com.intellij.xdebugger.XSourcePositionWrapper +import com.intellij.xdebugger.frame.* +import com.intellij.xdebugger.frame.presentation.XKeywordValuePresentation +import com.intellij.xdebugger.frame.presentation.XNumericValuePresentation +import com.intellij.xdebugger.frame.presentation.XStringValuePresentation +import com.intellij.xdebugger.frame.presentation.XValuePresentation +import org.jetbrains.concurrency.* +import org.jetbrains.debugger.values.* +import java.util.* +import java.util.concurrent.atomic.AtomicBoolean +import java.util.regex.Pattern +import javax.swing.Icon + +fun VariableView(variable: Variable, context: VariableContext): VariableView = VariableView(variable.name, variable, context) + +class VariableView(override val variableName: String, private val variable: Variable, private val context: VariableContext) : XNamedValue(variableName), VariableContext { + @Volatile private var value: Value? = null + // lazy computed + private var _memberFilter: MemberFilter? = null + + @Volatile private var remainingChildren: List<Variable>? = null + @Volatile private var remainingChildrenOffset: Int = 0 + + override fun watchableAsEvaluationExpression(): Boolean = context.watchableAsEvaluationExpression() + + override val viewSupport: DebuggerViewSupport + get() = context.viewSupport + + override val parent: VariableContext = context + + override val memberFilter: Promise<MemberFilter> + get() = context.viewSupport.getMemberFilter(this) + + override val evaluateContext: EvaluateContext + get() = context.evaluateContext + + override val scope: Scope? + get() = context.scope + + override val vm: Vm? + get() = context.vm + + override fun computePresentation(node: XValueNode, place: XValuePlace) { + value = variable.value + if (value != null) { + computePresentation(value!!, node) + return + } + + if (variable !is ObjectProperty || variable.getter == null) { + // it is "used" expression (WEB-6779 Debugger/Variables: Automatically show used variables) + evaluateContext.evaluate(variable.name) + .onSuccess(node) { + if (it.wasThrown) { + setEvaluatedValue(viewSupport.transformErrorOnGetUsedReferenceValue(it.value, null), null, node) + } + else { + value = it.value + computePresentation(it.value, node) + } + } + .onError(node) { setEvaluatedValue(viewSupport.transformErrorOnGetUsedReferenceValue(null, it.message), it.message, node) } + return + } + + node.setPresentation(null, object : XValuePresentation() { + override fun renderValue(renderer: XValuePresentation.XValueTextRenderer) { + renderer.renderValue("\u2026") + } + }, false) + node.setFullValueEvaluator(object : XFullValueEvaluator(" (invoke getter)") { + override fun startEvaluation(callback: XFullValueEvaluator.XFullValueEvaluationCallback) { + var valueModifier = variable.valueModifier + var nonProtoContext = context + while (nonProtoContext is VariableView && nonProtoContext.variableName == PROTOTYPE_PROP) { + valueModifier = nonProtoContext.variable.valueModifier + nonProtoContext = nonProtoContext.parent + } + valueModifier!!.evaluateGet(variable, evaluateContext) + .onSuccess(node) { + callback.evaluated("") + setEvaluatedValue(it, null, node) + } + } + }.setShowValuePopup(false)) + } + + private fun setEvaluatedValue(value: Value?, error: String?, node: XValueNode) { + if (value == null) { + node.setPresentation(AllIcons.Debugger.Db_primitive, null, error ?: "Internal Error", false) + } + else { + this.value = value + computePresentation(value, node) + } + } + + private fun computePresentation(value: Value, node: XValueNode) { + when (value.type) { + ValueType.OBJECT, ValueType.NODE -> context.viewSupport.computeObjectPresentation((value as ObjectValue), variable, context, node, icon) + + ValueType.FUNCTION -> node.setPresentation(icon, ObjectValuePresentation(trimFunctionDescription(value)), true) + + ValueType.ARRAY -> context.viewSupport.computeArrayPresentation(value, variable, context, node, icon) + + ValueType.BOOLEAN, ValueType.NULL, ValueType.UNDEFINED -> node.setPresentation(icon, XKeywordValuePresentation(value.valueString!!), false) + + ValueType.NUMBER, ValueType.BIGINT -> node.setPresentation(icon, createNumberPresentation(value.valueString!!), false) + + ValueType.STRING -> { + node.setPresentation(icon, XStringValuePresentation(value.valueString!!), false) + // isTruncated in terms of debugger backend, not in our terms (i.e. sometimes we cannot control truncation), + // so, even in case of StringValue, we check value string length + if ((value is StringValue && value.isTruncated) || value.valueString!!.length > XValueNode.MAX_VALUE_LENGTH) { + node.setFullValueEvaluator(MyFullValueEvaluator(value)) + } + } + + else -> node.setPresentation(icon, null, value.valueString!!, true) + } + } + + override fun computeChildren(node: XCompositeNode) { + node.setAlreadySorted(true) + + if (value !is ObjectValue) { + node.addChildren(XValueChildrenList.EMPTY, true) + return + } + + val list = remainingChildren + if (list != null) { + val to = Math.min(remainingChildrenOffset + XCompositeNode.MAX_CHILDREN_TO_SHOW, list.size) + val isLast = to == list.size + node.addChildren(createVariablesList(list, remainingChildrenOffset, to, this, _memberFilter), isLast) + if (!isLast) { + node.tooManyChildren(list.size - to) + remainingChildrenOffset += XCompositeNode.MAX_CHILDREN_TO_SHOW + } + return + } + + val objectValue = value as ObjectValue + val hasNamedProperties = objectValue.hasProperties() != ThreeState.NO + val hasIndexedProperties = objectValue.hasIndexedProperties() != ThreeState.NO + val promises = SmartList<Promise<*>>() + val additionalProperties = viewSupport.computeAdditionalObjectProperties(objectValue, variable, this, node) + if (additionalProperties != null) { + promises.add(additionalProperties) + } + + // we don't support indexed properties if additional properties added - behavior is undefined if object has indexed properties and additional properties also specified + if (hasIndexedProperties) { + promises.add(computeIndexedProperties(objectValue as ArrayValue, node, !hasNamedProperties && additionalProperties == null)) + } + + if (hasNamedProperties) { + // named properties should be added after additional properties + if (additionalProperties == null || additionalProperties.state != Promise.State.PENDING) { + promises.add(computeNamedProperties(objectValue, node, !hasIndexedProperties && additionalProperties == null)) + } + else { + promises.add(additionalProperties.thenAsync(node) { computeNamedProperties(objectValue, node, true) }) + } + } + + if (hasIndexedProperties == hasNamedProperties || additionalProperties != null) { + promises.all().processed(node) { node.addChildren(XValueChildrenList.EMPTY, true) } + } + } + + abstract class ObsolescentIndexedVariablesConsumer(protected val node: XCompositeNode) : IndexedVariablesConsumer() { + override val isObsolete: Boolean + get() = node.isObsolete + } + + private fun computeIndexedProperties(value: ArrayValue, node: XCompositeNode, isLastChildren: Boolean): Promise<*> { + return value.getIndexedProperties(0, value.length, XCompositeNode.MAX_CHILDREN_TO_SHOW, object : ObsolescentIndexedVariablesConsumer(node) { + override fun consumeRanges(ranges: IntArray?) { + if (ranges == null) { + val groupList = XValueChildrenList() + addGroups(value, ::lazyVariablesGroup, groupList, 0, value.length, XCompositeNode.MAX_CHILDREN_TO_SHOW, this@VariableView) + node.addChildren(groupList, isLastChildren) + } + else { + addRanges(value, ranges, node, this@VariableView, isLastChildren) + } + } + + override fun consumeVariables(variables: List<Variable>) { + node.addChildren(createVariablesList(variables, this@VariableView, null), isLastChildren) + } + }) + } + + private fun computeNamedProperties(value: ObjectValue, node: XCompositeNode, isLastChildren: Boolean) = processVariables(this, value.properties, node) { memberFilter, variables -> + _memberFilter = memberFilter + + if (value.type == ValueType.ARRAY && value !is ArrayValue) { + computeArrayRanges(variables, node) + return@processVariables + } + + var functionValue = value as? FunctionValue + if (functionValue != null && (functionValue.hasScopes() == ThreeState.NO)) { + functionValue = null + } + + remainingChildren = processNamedObjectProperties(variables, node, this@VariableView, memberFilter, XCompositeNode.MAX_CHILDREN_TO_SHOW, isLastChildren && functionValue == null) + if (remainingChildren != null) { + remainingChildrenOffset = XCompositeNode.MAX_CHILDREN_TO_SHOW + } + + if (functionValue != null) { + // we pass context as variable context instead of this variable value - we cannot watch function scopes variables, so, this variable name doesn't matter + node.addChildren(XValueChildrenList.bottomGroup(FunctionScopesValueGroup(functionValue, context)), isLastChildren && remainingChildren == null) + } + } + + private fun computeArrayRanges(properties: List<Variable>, node: XCompositeNode) { + val variables = filterAndSort(properties, _memberFilter!!) + var count = variables.size + val bucketSize = XCompositeNode.MAX_CHILDREN_TO_SHOW + if (count <= bucketSize) { + node.addChildren(createVariablesList(variables, this, null), true) + return + } + + while (count > 0) { + if (Character.isDigit(variables.get(count - 1).name[0])) { + break + } + count-- + } + + val groupList = XValueChildrenList() + if (count > 0) { + addGroups(variables, ::createArrayRangeGroup, groupList, 0, count, bucketSize, this) + } + + var notGroupedVariablesOffset: Int + if ((variables.size - count) > bucketSize) { + notGroupedVariablesOffset = variables.size + while (notGroupedVariablesOffset > 0) { + if (!variables.get(notGroupedVariablesOffset - 1).name.startsWith("__")) { + break + } + notGroupedVariablesOffset-- + } + + if (notGroupedVariablesOffset > 0) { + addGroups(variables, ::createArrayRangeGroup, groupList, count, notGroupedVariablesOffset, bucketSize, this) + } + } + else { + notGroupedVariablesOffset = count + } + + for (i in notGroupedVariablesOffset..variables.size - 1) { + val variable = variables.get(i) + groupList.add(VariableView(_memberFilter!!.rawNameToSource(variable), variable, this)) + } + + node.addChildren(groupList, true) + } + + private val icon: Icon + get() = getIcon(value!!) + + override fun getModifier(): XValueModifier? { + if (!variable.isMutable) { + return null + } + + return object : XValueModifier() { + override fun getInitialValueEditorText(): String? { + if (value!!.type == ValueType.STRING) { + val string = value!!.valueString!! + val builder = StringBuilder(string.length) + builder.append('"') + StringUtil.escapeStringCharacters(string.length, string, builder) + builder.append('"') + return builder.toString() + } + else { + return if (value!!.type.isObjectType) null else value!!.valueString + } + } + + override fun setValue(expression: XExpression, callback: XValueModifier.XModificationCallback) { + variable.valueModifier!!.setValue(variable, expression.expression, evaluateContext) + .doneRun { + value = null + callback.valueModified() + } + .onError { callback.errorOccurred(it.message!!) } + } + } + } + + fun getValue(): Value? = variable.value + + override fun canNavigateToSource(): Boolean = value is FunctionValue && value?.valueString?.contains("[native code]") != true + || viewSupport.canNavigateToSource(variable, context) + + override fun computeSourcePosition(navigatable: XNavigatable) { + if (value is FunctionValue) { + (value as FunctionValue).resolve() + .onSuccess { function -> + vm!!.scriptManager.getScript(function) + .onSuccess { + navigatable.setSourcePosition(it?.let { viewSupport.getSourceInfo(null, it, function.openParenLine, function.openParenColumn) }?.let { + object : XSourcePositionWrapper(it) { + override fun createNavigatable(project: Project): Navigatable { + return PsiVisitors.visit(myPosition, project) { _, element, _, _ -> + // element will be "open paren", but we should navigate to function name, + // we cannot use specific PSI type here (like JSFunction), so, we try to find reference expression (i.e. name expression) + var referenceCandidate: PsiElement? = element + var psiReference: PsiElement? = null + while (true) { + referenceCandidate = referenceCandidate?.prevSibling ?: break + if (referenceCandidate is PsiReference) { + psiReference = referenceCandidate + break + } + } + + if (psiReference == null) { + referenceCandidate = element.parent + while (true) { + referenceCandidate = referenceCandidate?.prevSibling ?: break + if (referenceCandidate is PsiReference) { + psiReference = referenceCandidate + break + } + } + } + + (if (psiReference == null) element.navigationElement else psiReference.navigationElement) as? Navigatable + } ?: super.createNavigatable(project) + } + } + }) + } + } + } + else { + viewSupport.computeSourcePosition(variableName, value!!, variable, context, navigatable) + } + } + + override fun computeInlineDebuggerData(callback: XInlineDebuggerDataCallback): ThreeState = viewSupport.computeInlineDebuggerData(variableName, variable, context, callback) + + override fun getEvaluationExpression(): String? { + if (!watchableAsEvaluationExpression()) { + return null + } + if (context.variableName == null) return variable.name // top level watch expression, may be call etc. + + val list = SmartList<String>() + addVarName(list, parent, variable.name) + + var parent: VariableContext? = context + while (parent != null && parent.variableName != null) { + addVarName(list, parent.parent, parent.variableName!!) + parent = parent.parent + } + return context.viewSupport.propertyNamesToString(list, false) + } + + private fun addVarName(list: SmartList<String>, parent: VariableContext?, name: String) { + if (parent == null || parent.variableName != null) list.add(name) + else list.addAll(name.split(".").reversed()) + } + + private class MyFullValueEvaluator(private val value: Value) : XFullValueEvaluator(if (value is StringValue) value.length else value.valueString!!.length) { + override fun startEvaluation(callback: XFullValueEvaluator.XFullValueEvaluationCallback) { + if (value !is StringValue || !value.isTruncated) { + callback.evaluated(value.valueString!!) + return + } + + val evaluated = AtomicBoolean() + value.fullString + .onSuccess { + if (!callback.isObsolete && evaluated.compareAndSet(false, true)) { + callback.evaluated(value.valueString!!) + } + } + .onError { callback.errorOccurred(it.message!!) } + } + } + + companion object { + fun setObjectPresentation(value: ObjectValue, icon: Icon, node: XValueNode) { + node.setPresentation(icon, ObjectValuePresentation(getObjectValueDescription(value)), value.hasProperties() != ThreeState.NO) + } + + fun setArrayPresentation(value: Value, context: VariableContext, icon: Icon, node: XValueNode) { + assert(value.type == ValueType.ARRAY) + + if (value is ArrayValue) { + val length = value.length + node.setPresentation(icon, ArrayPresentation(length, value.className), length > 0) + return + } + + val valueString = value.valueString + // only WIP reports normal description + if (valueString != null && (valueString.endsWith(")") || valueString.endsWith(']')) && + ARRAY_DESCRIPTION_PATTERN.matcher(valueString).find()) { + node.setPresentation(icon, null, valueString, true) + } + else { + context.evaluateContext.evaluate("a.length", Collections.singletonMap<String, Any>("a", value), false) + .onSuccess(node) { node.setPresentation(icon, null, "Array[${it.value.valueString}]", true) } + .onError(node) { + logger<VariableView>().error("Failed to evaluate array length: $it") + node.setPresentation(icon, null, valueString ?: "Array", true) + } + } + } + + fun getIcon(value: Value): Icon { + val type = value.type + return when (type) { + ValueType.FUNCTION -> AllIcons.Nodes.Function + ValueType.ARRAY -> AllIcons.Debugger.Db_array + else -> if (type.isObjectType) AllIcons.Debugger.Value else AllIcons.Debugger.Db_primitive + } + } + } +} + +fun getClassName(value: ObjectValue): String { + val className = value.className + return if (className.isNullOrEmpty()) "Object" else className!! +} + +fun getObjectValueDescription(value: ObjectValue): String { + val description = value.valueString + return if (description.isNullOrEmpty()) getClassName(value) else description!! +} + +internal fun trimFunctionDescription(value: Value): String { + return trimFunctionDescription(value.valueString ?: return "") +} + +fun trimFunctionDescription(value: String): String { + var endIndex = 0 + while (endIndex < value.length && !StringUtil.isLineBreak(value[endIndex])) { + endIndex++ + } + while (endIndex > 0 && Character.isWhitespace(value[endIndex - 1])) { + endIndex-- + } + return value.substring(0, endIndex) +} + +private fun createNumberPresentation(value: String): XValuePresentation { + return if (value == PrimitiveValue.NA_N_VALUE || value == PrimitiveValue.INFINITY_VALUE) XKeywordValuePresentation(value) else XNumericValuePresentation(value) +} + +private val ARRAY_DESCRIPTION_PATTERN = Pattern.compile("^[a-zA-Z\\d]+[\\[(]\\d+[\\])]$") + +private class ArrayPresentation(length: Int, className: String?) : XValuePresentation() { + private val length = Integer.toString(length) + private val className = if (className.isNullOrEmpty()) "Array" else className!! + + override fun renderValue(renderer: XValuePresentation.XValueTextRenderer) { + renderer.renderSpecialSymbol(className) + renderer.renderSpecialSymbol("[") + renderer.renderSpecialSymbol(length) + renderer.renderSpecialSymbol("]") + } +} + +private const val PROTOTYPE_PROP = "__proto__"
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/VariablesGroup.kt b/platform/script-debugger/debugger-ui/src/VariablesGroup.kt new file mode 100644 index 00000000..8726e4b9 --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/VariablesGroup.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.xdebugger.frame.XCompositeNode +import com.intellij.xdebugger.frame.XValueGroup + +internal class VariablesGroup(private val start: Int, private val end: Int, private val variables: List<Variable>, private val context: VariableContext, name: String) : XValueGroup(name) { + constructor(name: String, variables: List<Variable>, context: VariableContext) : this(0, variables.size, variables, context, name) { + } + + override fun computeChildren(node: XCompositeNode) { + node.setAlreadySorted(true) + node.addChildren(createVariablesList(variables, start, end, context, null), true) + } +} + +internal fun createArrayRangeGroup(variables: List<Variable>, start: Int, end: Int, variableContext: VariableContext): VariablesGroup { + val name = "[${variables[start].name} \u2026 ${variables[end - 1].name}]" + return VariablesGroup(start, end, variables, variableContext, name) +} diff --git a/platform/script-debugger/debugger-ui/src/VmConnection.kt b/platform/script-debugger/debugger-ui/src/VmConnection.kt new file mode 100644 index 00000000..082e776a --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/VmConnection.kt @@ -0,0 +1,110 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger.connection + +import com.intellij.ide.browsers.WebBrowser +import com.intellij.openapi.Disposable +import com.intellij.openapi.util.Disposer +import com.intellij.util.EventDispatcher +import com.intellij.util.containers.ContainerUtil +import com.intellij.util.io.socketConnection.ConnectionState +import com.intellij.util.io.socketConnection.ConnectionStatus +import com.intellij.util.io.socketConnection.SocketConnectionListener +import org.jetbrains.annotations.TestOnly +import org.jetbrains.concurrency.* +import org.jetbrains.debugger.DebugEventListener +import org.jetbrains.debugger.Vm +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference +import javax.swing.event.HyperlinkListener + +abstract class VmConnection<T : Vm> : Disposable { + open val browser: WebBrowser? = null + + private val stateRef = AtomicReference(ConnectionState(ConnectionStatus.NOT_CONNECTED)) + + protected open val dispatcher: EventDispatcher<DebugEventListener> = EventDispatcher.create(DebugEventListener::class.java) + private val connectionDispatcher = ContainerUtil.createLockFreeCopyOnWriteList<(ConnectionState) -> Unit>() + + @Volatile var vm: T? = null + protected set + + private val opened = AsyncPromise<T>() + private val closed = AtomicBoolean() + + val state: ConnectionState + get() = stateRef.get() + + fun addDebugListener(listener: DebugEventListener) { + dispatcher.addListener(listener) + } + + @TestOnly + fun opened(): Promise<*> = opened + + fun executeOnStart(consumer: (vm: T) -> Unit) { + opened.then(consumer) + } + + protected fun setState(status: ConnectionStatus, message: String? = null, messageLinkListener: HyperlinkListener? = null) { + val newState = ConnectionState(status, message, messageLinkListener) + val oldState = stateRef.getAndSet(newState) + if (oldState == null || oldState.status != status) { + if (status == ConnectionStatus.CONNECTION_FAILED) { + opened.setError(newState.message) + } + for (listener in connectionDispatcher) { + listener(newState) + } + } + } + + fun stateChanged(listener: (ConnectionState) -> Unit) { + connectionDispatcher.add(listener) + } + + // backward compatibility, go debugger + fun addListener(listener: SocketConnectionListener) { + stateChanged { listener.statusChanged(it.status) } + } + + protected val debugEventListener: DebugEventListener + get() = dispatcher.multicaster + + protected open fun startProcessing() { + vm?.let { opened.setResult(it) } + } + + fun close(message: String?, status: ConnectionStatus) { + if (!closed.compareAndSet(false, true)) { + return + } + + if (opened.isPending) { + opened.setError("closed") + } + setState(status, message) + Disposer.dispose(this, false) + } + + override fun dispose() { + vm = null + } + + open fun detachAndClose(): Promise<*> { + if (opened.isPending) { + opened.setError(createError("detached and closed")) + } + + val currentVm = vm + val callback: Promise<*> + if (currentVm == null) { + callback = nullPromise() + } + else { + vm = null + callback = currentVm.attachStateManager.detach() + } + close(null, ConnectionStatus.DISCONNECTED) + return callback + } +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/com/intellij/javascript/debugger/ExpressionInfoFactory.kt b/platform/script-debugger/debugger-ui/src/com/intellij/javascript/debugger/ExpressionInfoFactory.kt new file mode 100644 index 00000000..c88042d1 --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/com/intellij/javascript/debugger/ExpressionInfoFactory.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.javascript.debugger + +import com.intellij.openapi.editor.Document +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiElement +import com.intellij.xdebugger.evaluation.ExpressionInfo +import org.jetbrains.concurrency.Promise + +interface ExpressionInfoFactory { + fun create(element: PsiElement, document: Document): Promise<ExpressionInfo> + + fun createNameMapper(file: VirtualFile, document: Document): NameMapper? +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/com/intellij/javascript/debugger/NameMapper.kt b/platform/script-debugger/debugger-ui/src/com/intellij/javascript/debugger/NameMapper.kt new file mode 100644 index 00000000..c75eae1f --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/com/intellij/javascript/debugger/NameMapper.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.javascript.debugger + +import com.google.common.base.CharMatcher +import com.intellij.openapi.editor.Document +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiNamedElement +import gnu.trove.THashMap +import org.jetbrains.debugger.sourcemap.MappingEntry +import org.jetbrains.debugger.sourcemap.Mappings +import org.jetbrains.debugger.sourcemap.SourceMap +import org.jetbrains.rpc.LOG + +private val S1 = ",()[]{}=" +// don't trim trailing .&: - could be part of expression +private val OPERATOR_TRIMMER = CharMatcher.INVISIBLE.or(CharMatcher.anyOf(S1)) + +val NAME_TRIMMER: CharMatcher = CharMatcher.INVISIBLE.or(CharMatcher.anyOf(S1 + ".&:")) + +// generateVirtualFile only for debug purposes +open class NameMapper(private val document: Document, private val transpiledDocument: Document, private val sourceMappings: Mappings, protected val sourceMap: SourceMap, private val transpiledFile: VirtualFile? = null) { + var rawNameToSource: MutableMap<String, String>? = null + private set + + // PsiNamedElement, JSVariable for example + // returns generated name + open fun map(identifierOrNamedElement: PsiElement): String? { + return doMap(identifierOrNamedElement, false) + } + + protected fun doMap(identifierOrNamedElement: PsiElement, mapBySourceCode: Boolean): String? { + val offset = identifierOrNamedElement.textOffset + val line = document.getLineNumber(offset) + + val sourceEntryIndex = sourceMappings.indexOf(line, offset - document.getLineStartOffset(line)) + if (sourceEntryIndex == -1) { + return null + } + + val sourceEntry = sourceMappings.getByIndex(sourceEntryIndex) + val next = sourceMappings.getNextOnTheSameLine(sourceEntryIndex, false) + if (next != null && sourceMappings.getColumn(next) == sourceMappings.getColumn(sourceEntry)) { + warnSeveralMapping(identifierOrNamedElement) + return null + } + + val generatedName: String? + try { + generatedName = extractName(getGeneratedName(transpiledDocument, sourceMap, sourceEntry)) + } + catch (e: IndexOutOfBoundsException) { + LOG.warn("Cannot get generated name: source entry (${sourceEntry.generatedLine}, ${sourceEntry.generatedColumn}). Transpiled File: " + transpiledFile?.path) + return null + } + if (generatedName == null || generatedName.isEmpty()) { + return null + } + + var sourceName = sourceEntry.name + if (sourceName == null || mapBySourceCode) { + sourceName = (identifierOrNamedElement as? PsiNamedElement)?.name ?: identifierOrNamedElement.text ?: sourceName ?: return null + } + + addMapping(generatedName, sourceName) + return generatedName + } + + fun addMapping(generatedName: String, sourceName: String) { + if (rawNameToSource == null) { + rawNameToSource = THashMap<String, String>() + } + rawNameToSource!!.put(generatedName, sourceName) + } + + protected open fun extractName(rawGeneratedName: CharSequence):String? = NAME_TRIMMER.trimFrom(rawGeneratedName) + + companion object { + fun trimName(rawGeneratedName: CharSequence, isLastToken: Boolean): String? = (if (isLastToken) NAME_TRIMMER else OPERATOR_TRIMMER).trimFrom(rawGeneratedName) + } +} + +fun warnSeveralMapping(element: PsiElement) { + // see https://dl.dropboxusercontent.com/u/43511007/s/Screen%20Shot%202015-01-21%20at%2020.33.44.png + // var1 mapped to the whole "var c, notes, templates, ..." expression text + unrelated text " ;" + LOG.warn("incorrect sourcemap, several mappings for named element ${element.text}") +} + +private fun getGeneratedName(document: Document, sourceMap: SourceMap, sourceEntry: MappingEntry): CharSequence { + val lineStartOffset = document.getLineStartOffset(sourceEntry.generatedLine) + val nextGeneratedMapping = sourceMap.generatedMappings.getNextOnTheSameLine(sourceEntry) + val endOffset: Int + if (nextGeneratedMapping == null) { + endOffset = document.getLineEndOffset(sourceEntry.generatedLine) + } + else { + endOffset = lineStartOffset + nextGeneratedMapping.generatedColumn + } + return document.immutableCharSequence.subSequence(lineStartOffset + sourceEntry.generatedColumn, endOffset) +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/com/intellij/javascript/debugger/execution/DebuggableProgramRunner.kt b/platform/script-debugger/debugger-ui/src/com/intellij/javascript/debugger/execution/DebuggableProgramRunner.kt new file mode 100644 index 00000000..cc190be2 --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/com/intellij/javascript/debugger/execution/DebuggableProgramRunner.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.javascript.debugger.execution + +import com.intellij.execution.ExecutionResult +import com.intellij.execution.configurations.RunProfile +import com.intellij.execution.configurations.RunProfileState +import com.intellij.execution.configurations.RunnerSettings +import com.intellij.execution.executors.DefaultDebugExecutor +import com.intellij.execution.runners.AsyncProgramRunner +import com.intellij.execution.runners.DebuggableRunProfileState +import com.intellij.execution.runners.ExecutionEnvironment +import com.intellij.execution.ui.RunContentDescriptor +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.xdebugger.XDebugProcess +import com.intellij.xdebugger.XDebugProcessStarter +import com.intellij.xdebugger.XDebugSession +import com.intellij.xdebugger.XDebuggerManager +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.resolvedPromise +import org.jetbrains.debugger.DebuggableRunConfiguration + +open class DebuggableProgramRunner : AsyncProgramRunner<RunnerSettings>() { + override fun execute(environment: ExecutionEnvironment, state: RunProfileState): Promise<RunContentDescriptor?> { + FileDocumentManager.getInstance().saveAllDocuments() + val configuration = environment.runProfile as DebuggableRunConfiguration + val socketAddress = configuration.computeDebugAddress(state) + val starter = { executionResult: ExecutionResult? -> + startSession(environment) { configuration.createDebugProcess(socketAddress, it, executionResult, environment) }.runContentDescriptor + } + @Suppress("IfThenToElvis") + if (state is DebuggableRunProfileState) { + return state.execute(socketAddress.port).then(starter) + } + else { + return resolvedPromise(starter(null)) + } + } + + override fun getRunnerId(): String = "debuggableProgram" + + override fun canRun(executorId: String, profile: RunProfile): Boolean { + return DefaultDebugExecutor.EXECUTOR_ID == executorId && profile is DebuggableRunConfiguration && profile.canRun(executorId, profile) + } +} + +inline fun startSession(environment: ExecutionEnvironment, crossinline starter: (session: XDebugSession) -> XDebugProcess): XDebugSession { + return XDebuggerManager.getInstance(environment.project).startSession(environment, xDebugProcessStarter(starter)) +} + +inline fun xDebugProcessStarter(crossinline starter: (session: XDebugSession) -> XDebugProcess): XDebugProcessStarter = object : XDebugProcessStarter() { + override fun start(session: XDebugSession) = starter(session) +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/com/jetbrains/javascript/debugger/FileUrlMapper.kt b/platform/script-debugger/debugger-ui/src/com/jetbrains/javascript/debugger/FileUrlMapper.kt new file mode 100644 index 00000000..861a3b88 --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/com/jetbrains/javascript/debugger/FileUrlMapper.kt @@ -0,0 +1,34 @@ +package com.jetbrains.javascript.debugger + +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.fileEditor.OpenFileDescriptor +import com.intellij.openapi.fileTypes.FileType +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.pom.Navigatable +import com.intellij.util.Url +import org.jetbrains.debugger.sourcemap.SourceFileResolver + +abstract class FileUrlMapper { + companion object { + @JvmField + val EP_NAME: ExtensionPointName<FileUrlMapper> = ExtensionPointName.create<FileUrlMapper>("com.jetbrains.fileUrlMapper") + } + + open fun getUrls(file: VirtualFile, project: Project, currentAuthority: String?): List<Url> = emptyList() + + /** + * Optional to implement, useful if default navigation position to source file is not equals to 0:0 (java file for example) + */ + open fun getNavigatable(url: Url, project: Project, requestor: Url?): Navigatable? = getFile(url, project, requestor)?.let { OpenFileDescriptor(project, it) } + + abstract fun getFile(url: Url, project: Project, requestor: Url?): VirtualFile? + + /** + * Optional to implement, sometimes you cannot build URL, but can match. + * Lifetime: resolve session lifetime. Could be called multiple times: n <= total sourcemap count + */ + open fun createSourceResolver(file: VirtualFile, project: Project): SourceFileResolver? = null + + open fun getFileType(url: Url): FileType? = null +} diff --git a/platform/script-debugger/debugger-ui/src/com/jetbrains/javascript/debugger/JavaScriptDebugAware.kt b/platform/script-debugger/debugger-ui/src/com/jetbrains/javascript/debugger/JavaScriptDebugAware.kt new file mode 100644 index 00000000..007cd6dc --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/com/jetbrains/javascript/debugger/JavaScriptDebugAware.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.javascript.debugger + +import com.intellij.javascript.debugger.ExpressionInfoFactory +import com.intellij.javascript.debugger.NameMapper +import com.intellij.openapi.editor.Document +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.fileTypes.FileType +import com.intellij.openapi.fileTypes.LanguageFileType +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.xdebugger.breakpoints.XLineBreakpointType +import com.intellij.xdebugger.evaluation.ExpressionInfo +import org.jetbrains.concurrency.Promise +import org.jetbrains.debugger.MemberFilter + +/** + * @see com.intellij.javascript.debugger.JavaScriptDebugAwareBase + */ +abstract class JavaScriptDebugAware { + companion object { + val EP_NAME: ExtensionPointName<JavaScriptDebugAware> = ExtensionPointName.create<JavaScriptDebugAware>("com.jetbrains.javaScriptDebugAware") + + @JvmStatic + fun isBreakpointAware(fileType: FileType): Boolean { + val aware = getBreakpointAware(fileType) + return aware != null && aware.breakpointTypeClass == null + } + + fun getBreakpointAware(fileType: FileType): JavaScriptDebugAware? { + for (debugAware in EP_NAME.extensions) { + if (fileType == debugAware.fileType) { + return debugAware + } + } + return null + } + } + + protected open val fileType: LanguageFileType? + get() = null + + open val breakpointTypeClass: Class<out XLineBreakpointType<*>>? + get() = null + + /** + * Return false if you language could be natively executed in the VM + * You must not specify it and it doesn't matter if you use not own breakpoint type - (Kotlin or GWT use java breakpoint type, for example) + */ + open val isOnlySourceMappedBreakpoints: Boolean + get() = true + + fun canGetEvaluationInfo(file: PsiFile): Boolean = file.fileType == fileType + + open fun getEvaluationInfo(element: PsiElement, document: Document, expressionInfoFactory: ExpressionInfoFactory): Promise<ExpressionInfo?>? = null + + open fun createMemberFilter(nameMapper: NameMapper?, element: PsiElement, end: Int): MemberFilter? = null + + open fun getNavigationElementForSourcemapInspector(file: PsiFile): PsiElement? = null + + // return null if unsupported + // cannot be in MemberFilter because creation of MemberFilter could be async + // the problem - GWT mangles name (https://code.google.com/p/google-web-toolkit/issues/detail?id=9106 https://github.com/sdbg/sdbg/issues/6 https://youtrack.jetbrains.com/issue/IDEA-135356), but doesn't add name mappings + open fun normalizeMemberName(name: String): String? = null +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/CustomPropertiesValuePresentation.kt b/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/CustomPropertiesValuePresentation.kt new file mode 100644 index 00000000..6133321a --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/CustomPropertiesValuePresentation.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.openapi.editor.DefaultLanguageHighlighterColors +import com.intellij.xdebugger.XDebuggerBundle +import com.intellij.xdebugger.frame.XValueNode +import com.intellij.xdebugger.frame.presentation.XValuePresentation +import org.jetbrains.debugger.values.ObjectValue +import org.jetbrains.debugger.values.StringValue +import org.jetbrains.debugger.values.ValueType + +class CustomPropertiesValuePresentation(private val value: ObjectValue, private val properties: List<Variable>) : XValuePresentation() { + override fun renderValue(renderer: XValuePresentation.XValueTextRenderer) { + renderer.renderComment(getObjectValueDescription(value)) + renderer.renderSpecialSymbol(" {") + var isFirst = true + for (property in properties) { + if (isFirst) { + isFirst = false + } + else { + renderer.renderSpecialSymbol(", ") + } + + renderer.renderValue(property.name, DefaultLanguageHighlighterColors.INSTANCE_FIELD) + renderer.renderSpecialSymbol(": ") + + val value = property.value!! + when (value.type) { + ValueType.BOOLEAN, ValueType.NULL, ValueType.UNDEFINED, ValueType.SYMBOL -> renderer.renderKeywordValue(value.valueString!!) + + ValueType.NUMBER, ValueType.BIGINT -> renderer.renderNumericValue(value.valueString!!) + + ValueType.STRING -> { + val string = value.valueString + renderer.renderStringValue(string!!, "\"\\", XValueNode.MAX_VALUE_LENGTH) + val actualStringLength = (value as? StringValue)?.length ?: string.length + if (actualStringLength > XValueNode.MAX_VALUE_LENGTH) { + renderer.renderComment(XDebuggerBundle.message("node.text.ellipsis.truncated", actualStringLength)) + } + } + + ValueType.FUNCTION -> renderer.renderComment(trimFunctionDescription(value)) + + ValueType.OBJECT -> renderer.renderComment(getObjectValueDescription(value as ObjectValue)) + + else -> renderer.renderValue(value.valueString!!) + } + } + renderer.renderSpecialSymbol("}") + } +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/DebuggableRunConfiguration.java b/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/DebuggableRunConfiguration.java new file mode 100644 index 00000000..cc8fe28f --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/DebuggableRunConfiguration.java @@ -0,0 +1,54 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger; + +import com.intellij.execution.ExecutionException; +import com.intellij.execution.ExecutionResult; +import com.intellij.execution.configurations.RunConfiguration; +import com.intellij.execution.configurations.RunProfile; +import com.intellij.execution.configurations.RunProfileState; +import com.intellij.execution.runners.ExecutionEnvironment; +import com.intellij.util.net.NetUtils; +import com.intellij.xdebugger.XDebugProcess; +import com.intellij.xdebugger.XDebugSession; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; + +public interface DebuggableRunConfiguration extends RunConfiguration { + @NotNull + default InetSocketAddress computeDebugAddress(RunProfileState state) throws ExecutionException { + try { + return new InetSocketAddress(InetAddress.getLoopbackAddress(), NetUtils.findAvailableSocketPort()); + } + catch (IOException e) { + throw new ExecutionException("Cannot find available port", e); + } + } + + @NotNull + XDebugProcess createDebugProcess(@NotNull InetSocketAddress socketAddress, + @NotNull XDebugSession session, + @Nullable ExecutionResult executionResult, + @NotNull ExecutionEnvironment environment) throws ExecutionException; + + default boolean canRun(@NotNull String executorId, @NotNull RunProfile profile) { + return true; + } +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/DebuggerSupportUtils.java b/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/DebuggerSupportUtils.java new file mode 100644 index 00000000..9b82da06 --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/DebuggerSupportUtils.java @@ -0,0 +1,31 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger; + +import com.intellij.psi.PsiElement; +import com.intellij.xdebugger.XDebuggerUtil; +import com.intellij.xdebugger.XSourcePosition; +import org.jetbrains.annotations.Nullable; + +public final class DebuggerSupportUtils { + @Nullable + public static XSourcePosition calcSourcePosition(@Nullable PsiElement element) { + if (element != null) { + return XDebuggerUtil.getInstance().createPositionByElement(element.getNavigationElement()); + } + return null; + } +} diff --git a/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/LazyVariablesGroup.kt b/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/LazyVariablesGroup.kt new file mode 100644 index 00000000..52ec2cfb --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/LazyVariablesGroup.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.xdebugger.frame.XCompositeNode +import com.intellij.xdebugger.frame.XValueChildrenList +import com.intellij.xdebugger.frame.XValueGroup +import org.jetbrains.debugger.values.ObjectValue +import org.jetbrains.debugger.values.ValueType +import java.util.* + +internal fun lazyVariablesGroup(variables: ObjectValue, start: Int, end: Int, context: VariableContext) = LazyVariablesGroup(variables, start, end, context) + +class LazyVariablesGroup(private val value: ObjectValue, private val startInclusive: Int, private val endInclusive: Int, private val context: VariableContext, private val componentType: ValueType? = null, private val sparse: Boolean = true) : XValueGroup(String.format("[%,d \u2026 %,d]", startInclusive, endInclusive)) { + override fun computeChildren(node: XCompositeNode) { + node.setAlreadySorted(true) + + val bucketThreshold = XCompositeNode.MAX_CHILDREN_TO_SHOW + if (!sparse && endInclusive - startInclusive > bucketThreshold) { + node.addChildren(XValueChildrenList.topGroups(computeNotSparseGroups(value, context, startInclusive, endInclusive + 1, bucketThreshold)), true) + return + } + + value.getIndexedProperties(startInclusive, endInclusive + 1, bucketThreshold, object : VariableView.ObsolescentIndexedVariablesConsumer(node) { + override fun consumeRanges(ranges: IntArray?) { + if (ranges == null) { + val groupList = XValueChildrenList() + addGroups(value, ::lazyVariablesGroup, groupList, startInclusive, endInclusive, XCompositeNode.MAX_CHILDREN_TO_SHOW, context) + node.addChildren(groupList, true) + } + else { + addRanges(value, ranges, node, context, true) + } + } + + override fun consumeVariables(variables: List<Variable>) { + node.addChildren(createVariablesList(variables, context, null), true) + } + }, componentType) + } +} + +fun computeNotSparseGroups(value: ObjectValue, context: VariableContext, _fromInclusive: Int, toExclusive: Int, bucketThreshold: Int): List<XValueGroup> { + var fromInclusive = _fromInclusive + val size = toExclusive - fromInclusive + val bucketSize = Math.pow(bucketThreshold.toDouble(), Math.ceil(Math.log(size.toDouble()) / Math.log(bucketThreshold.toDouble())) - 1).toInt() + val groupList = ArrayList<XValueGroup>(Math.ceil((size / bucketSize).toDouble()).toInt()) + while (fromInclusive < toExclusive) { + groupList.add(LazyVariablesGroup(value, fromInclusive, fromInclusive + (Math.min(bucketSize, toExclusive - fromInclusive) - 1), context, ValueType.NUMBER, false)) + fromInclusive += bucketSize + } + return groupList +} + +fun addRanges(value: ObjectValue, ranges: IntArray, node: XCompositeNode, context: VariableContext, isLast: Boolean) { + val groupList = XValueChildrenList(ranges.size / 2) + var i = 0 + val n = ranges.size + while (i < n) { + groupList.addTopGroup(LazyVariablesGroup(value, ranges[i], ranges[i + 1], context)) + i += 2 + } + node.addChildren(groupList, isLast) +} + +internal fun <T> addGroups(data: T, + groupFactory: (data: T, start: Int, end: Int, context: VariableContext) -> XValueGroup, + groupList: XValueChildrenList, + _from: Int, + limit: Int, + bucketSize: Int, + context: VariableContext) { + var from = _from + var to = Math.min(bucketSize, limit) + var done = false + do { + val groupFrom = from + var groupTo = to + + from += bucketSize + to = from + Math.min(bucketSize, limit - from) + + // don't create group for only one member + if (to - from == 1) { + groupTo++ + done = true + } + groupList.addTopGroup(groupFactory(data, groupFrom, groupTo, context)) + if (from >= limit) { + break + } + } + while (!done) +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/Location.java b/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/Location.java new file mode 100644 index 00000000..2dea7ccf --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/Location.java @@ -0,0 +1,93 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger; + +import com.intellij.util.Url; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * We use term "location" instead of "position" because webkit uses term "location" + */ +public final class Location { + private final Url url; + private final Script script; + + private final int line; + private final int column; + + public Location(@NotNull Url url, int line, int column) { + this.url = url; + this.line = line; + this.column = column; + script = null; + } + + public Location(@NotNull Script script, int line, int column) { + this.url = script.getUrl(); + this.line = line; + this.column = column; + this.script = script; + } + + public Location(@NotNull Url url, int line) { + this(url, line, -1); + } + + @NotNull + public Location withoutColumn() { + return script == null ? new Location(url, line) : new Location(script, line, -1); + } + + @NotNull + public Url getUrl() { + return url; + } + + @Nullable + public Script getScript() { + return script; + } + + public int getLine() { + return line; + } + + public int getColumn() { + return column; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Location location = (Location)o; + return column == location.column && line == location.line && url.equals(location.url); + } + + @Override + public int hashCode() { + int result = url.hashCode(); + result = 31 * result + line; + result = 31 * result + column; + return result; + } +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/MemberFilterWithNameMappings.kt b/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/MemberFilterWithNameMappings.kt new file mode 100644 index 00000000..c30d7c87 --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/MemberFilterWithNameMappings.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +open class MemberFilterWithNameMappings(private val rawNameToSource: Map<String, String> = emptyMap()) : MemberFilter { + override fun hasNameMappings(): Boolean = !rawNameToSource.isEmpty() + + override fun rawNameToSource(variable: Variable): String { + val name = variable.name + val sourceName = rawNameToSource.get(name) + return sourceName ?: normalizeMemberName(name) + } + + protected open fun normalizeMemberName(name: String): String = name + + override fun sourceNameToRaw(name: String): String? { + if (!hasNameMappings()) { + return null + } + + for ((key, value) in rawNameToSource) { + if (value == name) { + return key + } + } + return null + } +} diff --git a/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/ObjectValuePresentation.java b/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/ObjectValuePresentation.java new file mode 100644 index 00000000..8231b08a --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/ObjectValuePresentation.java @@ -0,0 +1,17 @@ +package org.jetbrains.debugger; + +import com.intellij.xdebugger.frame.presentation.XValuePresentation; +import org.jetbrains.annotations.NotNull; + +public class ObjectValuePresentation extends XValuePresentation { + private final String myValue; + + public ObjectValuePresentation(@NotNull String value) { + myValue = value; + } + + @Override + public void renderValue(@NotNull XValueTextRenderer renderer) { + renderer.renderComment(myValue); + } +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/PsiVisitors.java b/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/PsiVisitors.java new file mode 100644 index 00000000..96b80600 --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/PsiVisitors.java @@ -0,0 +1,83 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger; + +import com.intellij.openapi.application.ReadAction; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.project.Project; +import com.intellij.psi.*; +import com.intellij.psi.impl.source.tree.ForeignLeafPsiElement; +import com.intellij.psi.templateLanguages.OuterLanguageElement; +import com.intellij.util.DocumentUtil; +import com.intellij.xdebugger.XSourcePosition; +import org.jetbrains.annotations.NotNull; + +public final class PsiVisitors { + public static <RESULT> RESULT visit(@NotNull XSourcePosition position, @NotNull Project project, @NotNull Visitor<RESULT> visitor) { + return visit(position, project, visitor, null); + } + + /** + * Read action will be taken automatically + */ + public static <RESULT> RESULT visit(@NotNull XSourcePosition position, @NotNull Project project, @NotNull Visitor<? extends RESULT> visitor, RESULT defaultResult) { + return ReadAction.compute(()->{ + Document document = FileDocumentManager.getInstance().getDocument(position.getFile()); + PsiFile file = document == null || document.getTextLength() == 0 ? null : PsiDocumentManager.getInstance(project).getPsiFile(document); + if (file == null) { + return defaultResult; + } + + int positionOffset; + int column = position instanceof SourceInfo ? Math.max(((SourceInfo)position).getColumn(), 0) : 0; + try { + positionOffset = column == 0 ? DocumentUtil.getFirstNonSpaceCharOffset(document, position.getLine()) : document.getLineStartOffset(position.getLine()) + column; + } + catch (IndexOutOfBoundsException ignored) { + return defaultResult; + } + + PsiElement element = file.findElementAt(positionOffset); + return element == null ? defaultResult : visitor.visit(position, element, positionOffset, document); + }); + } + + public interface Visitor<RESULT> { + RESULT visit(@NotNull XSourcePosition position, @NotNull PsiElement element, int positionOffset, @NotNull Document document); + } + + public static abstract class FilteringPsiRecursiveElementWalkingVisitor extends PsiRecursiveElementWalkingVisitor { + @Override + public void visitElement(PsiElement element) { + if (!(element instanceof ForeignLeafPsiElement) && element.isPhysical()) { + super.visitElement(element); + } + } + + @Override + public void visitWhiteSpace(PsiWhiteSpace space) { + } + + @Override + public void visitComment(PsiComment comment) { + } + + @Override + public void visitOuterLanguageElement(OuterLanguageElement element) { + } + } +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/RejectErrorReporter.kt b/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/RejectErrorReporter.kt new file mode 100644 index 00000000..3d6ccc1e --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/RejectErrorReporter.kt @@ -0,0 +1,15 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger + +import com.intellij.xdebugger.XDebugSession +import org.jetbrains.concurrency.errorIfNotMessage +import org.jetbrains.rpc.LOG +import java.util.function.Consumer + +class RejectErrorReporter @JvmOverloads constructor(private val session: XDebugSession, private val description: String? = null) : Consumer<Throwable> { + override fun accept(error: Throwable) { + if (LOG.errorIfNotMessage(error)) { + session.reportError("${if (description == null) "" else "$description: "}${error.message}") + } + } +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/RemoteDebugConfiguration.java b/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/RemoteDebugConfiguration.java new file mode 100644 index 00000000..b76f76ad --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/RemoteDebugConfiguration.java @@ -0,0 +1,154 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ +package org.jetbrains.debugger; + +import com.intellij.execution.Executor; +import com.intellij.execution.configuration.EmptyRunProfileState; +import com.intellij.execution.configurations.ConfigurationFactory; +import com.intellij.execution.configurations.LocatableConfigurationBase; +import com.intellij.execution.configurations.RunConfiguration; +import com.intellij.execution.configurations.RunProfileState; +import com.intellij.execution.runners.ExecutionEnvironment; +import com.intellij.execution.runners.RunConfigurationWithSuppressedDefaultRunAction; +import com.intellij.openapi.options.SettingsEditor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.InvalidDataException; +import com.intellij.openapi.util.WriteExternalException; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.ui.GuiUtils; +import com.intellij.ui.PortField; +import com.intellij.util.ThreeState; +import com.intellij.util.ui.FormBuilder; +import com.intellij.util.xmlb.SerializationFilter; +import com.intellij.util.xmlb.SkipEmptySerializationFilter; +import com.intellij.util.xmlb.XmlSerializer; +import com.intellij.util.xmlb.annotations.Attribute; +import org.jdom.Element; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.net.InetAddress; +import java.net.InetSocketAddress; + +public abstract class RemoteDebugConfiguration extends LocatableConfigurationBase implements RunConfigurationWithSuppressedDefaultRunAction, DebuggableRunConfiguration { + private final SerializationFilter serializationFilter = new SkipEmptySerializationFilter() { + @Override + protected ThreeState accepts(@NotNull String name, @NotNull Object beanValue) { + return name.equals("port") ? ThreeState.fromBoolean(!beanValue.equals(defaultPort)) : ThreeState.UNSURE; + } + }; + + private String host; + + private int port; + private final int defaultPort; + + public RemoteDebugConfiguration(Project project, @NotNull ConfigurationFactory factory, String name, int defaultPort) { + super(project, factory, name); + + port = defaultPort; + this.defaultPort = defaultPort; + } + + @Nullable + @Attribute + public String getHost() { + return host; + } + + public void setHost(@Nullable String value) { + if (StringUtil.isEmpty(value) || value.equals("localhost") || value.equals("127.0.0.1")) { + host = null; + } + else { + host = value; + } + } + + @Attribute + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + @NotNull + @Override + public SettingsEditor<? extends RunConfiguration> getConfigurationEditor() { + return new RemoteDebugConfigurationSettingsEditor(); + } + + @Nullable + @Override + public RunProfileState getState(@NotNull Executor executor, @NotNull ExecutionEnvironment env) { + return EmptyRunProfileState.INSTANCE; + } + + @Override + public RunConfiguration clone() { + RemoteDebugConfiguration configuration = (RemoteDebugConfiguration)super.clone(); + configuration.host = host; + configuration.port = port; + return configuration; + } + + @Override + public void readExternal(@NotNull Element element) throws InvalidDataException { + super.readExternal(element); + + XmlSerializer.deserializeInto(this, element); + if (port <= 0) { + port = defaultPort; + } + } + + @Override + public void writeExternal(@NotNull Element element) throws WriteExternalException { + super.writeExternal(element); + + XmlSerializer.serializeInto(this, element, serializationFilter); + } + + @NotNull + @Override + public InetSocketAddress computeDebugAddress(RunProfileState state) { + if (host == null) { + return new InetSocketAddress(InetAddress.getLoopbackAddress(), port); + } + else { + return new InetSocketAddress(host, getPort()); + } + } + + private final class RemoteDebugConfigurationSettingsEditor extends SettingsEditor<RemoteDebugConfiguration> { + private final JTextField hostField; + private final PortField portField; + + RemoteDebugConfigurationSettingsEditor() { + hostField = GuiUtils.createUndoableTextField(); + portField = new PortField(defaultPort, 1024); + } + + @Override + protected void resetEditorFrom(@NotNull RemoteDebugConfiguration configuration) { + hostField.setText(StringUtil.notNullize(configuration.host, "localhost")); + portField.setNumber(configuration.port); + } + + @Override + protected void applyEditorTo(@NotNull RemoteDebugConfiguration configuration) { + configuration.setHost(hostField.getText()); + configuration.setPort(portField.getNumber()); + } + + @NotNull + @Override + protected JComponent createEditor() { + return FormBuilder.createFormBuilder().addLabeledComponent("&Host:", hostField).addLabeledComponent("&Port:", portField).getPanel(); + } + } +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/VariableViewBase.java b/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/VariableViewBase.java new file mode 100644 index 00000000..82d8e129 --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/VariableViewBase.java @@ -0,0 +1,18 @@ +package org.jetbrains.debugger; + +import com.intellij.xdebugger.frame.XNamedValue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.debugger.values.ValueType; + +// todo remove when Firefox implementation will use SDK +public abstract class VariableViewBase extends XNamedValue { + protected VariableViewBase(@NotNull String name) { + super(name); + } + + public abstract ValueType getValueType(); + + public boolean isDomPropertiesValue() { + return false; + } +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/Variables.kt b/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/Variables.kt new file mode 100644 index 00000000..6460cc6e --- /dev/null +++ b/platform/script-debugger/debugger-ui/src/org/jetbrains/debugger/Variables.kt @@ -0,0 +1,301 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.openapi.util.text.StringUtil +import com.intellij.util.SmartList +import com.intellij.xdebugger.frame.XCompositeNode +import com.intellij.xdebugger.frame.XValueChildrenList +import org.jetbrains.concurrency.Obsolescent +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.then +import org.jetbrains.concurrency.thenAsync +import org.jetbrains.debugger.values.ValueType +import java.util.* +import java.util.regex.Pattern + +private val UNNAMED_FUNCTION_PATTERN = Pattern.compile("^function[\\t ]*\\(") + +private val NATURAL_NAME_COMPARATOR = Comparator<Variable> { o1, o2 -> naturalCompare(o1.name, o2.name) } + +// start properties loading to achieve, possibly, parallel execution (properties loading & member filter computation) +fun processVariables(context: VariableContext, + variables: Promise<List<Variable>>, + obsolescent: Obsolescent, + consumer: (memberFilter: MemberFilter, variables: List<Variable>) -> Unit): Promise<Unit> { + return context.memberFilter + .thenAsync(obsolescent) { memberFilter -> + variables + .then(obsolescent) { + consumer(memberFilter, it) + } + } +} + +fun processScopeVariables(scope: Scope, + node: XCompositeNode, + context: VariableContext, + isLast: Boolean): Promise<Unit> { + return processVariables(context, scope.variablesHost.get(), node) { memberFilter, variables -> + val additionalVariables = memberFilter.additionalVariables + + val exceptionValue = context.vm?.suspendContextManager?.context?.exceptionData?.exceptionValue + val properties = ArrayList<Variable>(variables.size + additionalVariables.size + (if (exceptionValue == null) 0 else 1)) + + exceptionValue?.let { + properties.add(VariableImpl("Exception", it)) + } + + val functions = SmartList<Variable>() + for (variable in variables) { + if (memberFilter.isMemberVisible(variable) && variable.name != RECEIVER_NAME && variable.name != memberFilter.sourceNameToRaw(RECEIVER_NAME)) { + val value = variable.value + if (value != null && value.type == ValueType.FUNCTION && value.valueString != null && !UNNAMED_FUNCTION_PATTERN.matcher( + value.valueString).lookingAt()) { + functions.add(variable) + } + else { + properties.add(variable) + } + } + } + + addAditionalVariables(additionalVariables, properties, memberFilter) + + val comparator = if (memberFilter.hasNameMappings()) Comparator { o1, o2 -> + naturalCompare(memberFilter.rawNameToSource(o1), memberFilter.rawNameToSource(o2)) + } + else NATURAL_NAME_COMPARATOR + properties.sortWith(comparator) + functions.sortWith(comparator) + + if (!properties.isEmpty()) { + node.addChildren(createVariablesList(properties, context, memberFilter), functions.isEmpty() && isLast) + } + + if (!functions.isEmpty()) { + node.addChildren(XValueChildrenList.bottomGroup(VariablesGroup("Functions", functions, context)), isLast) + } + else if (isLast && properties.isEmpty()) { + node.addChildren(XValueChildrenList.EMPTY, true) + } + } +} + +fun processNamedObjectProperties(variables: List<Variable>, + node: XCompositeNode, + context: VariableContext, + memberFilter: MemberFilter, + maxChildrenToAdd: Int, + defaultIsLast: Boolean): List<Variable>? { + val list = filterAndSort(variables, memberFilter) + if (list.isEmpty()) { + if (defaultIsLast) { + node.addChildren(XValueChildrenList.EMPTY, true) + } + return null + } + + val to = Math.min(maxChildrenToAdd, list.size) + val isLast = to == list.size + node.addChildren(createVariablesList(list, 0, to, context, memberFilter), defaultIsLast && isLast) + if (isLast) { + return null + } + else { + node.tooManyChildren(list.size - to) + return list + } +} + +fun filterAndSort(variables: List<Variable>, memberFilter: MemberFilter): List<Variable> { + if (variables.isEmpty()) { + return emptyList() + } + + val additionalVariables = memberFilter.additionalVariables + val result = ArrayList<Variable>(variables.size + additionalVariables.size) + for (variable in variables) { + if (memberFilter.isMemberVisible(variable)) { + result.add(variable) + } + } + result.sortWith(NATURAL_NAME_COMPARATOR) + + addAditionalVariables(additionalVariables, result, memberFilter) + return result +} + +private fun addAditionalVariables(additionalVariables: Collection<Variable>, + result: MutableList<Variable>, + memberFilter: MemberFilter, + functions: MutableList<Variable>? = null) { + val oldSize = result.size + ol@ for (variable in additionalVariables) { + if (!memberFilter.isMemberVisible(variable)) continue + + for (i in 0..(oldSize - 1)) { + val vmVariable = result[i] + if (memberFilter.rawNameToSource(vmVariable) == memberFilter.rawNameToSource(variable)) { + // we prefer additionalVariable here because it is more smart variable (e.g. NavigatableVariable) + val vmValue = vmVariable.value + // to avoid evaluation, use vm value directly + if (vmValue != null) { + variable.value = vmValue + } + + result.set(i, variable) + continue@ol + } + } + + if (functions != null) { + for (function in functions) { + if (memberFilter.rawNameToSource(function) == memberFilter.rawNameToSource(variable)) { + continue@ol + } + } + } + + result.add(variable) + } +} + +// prefixed '_' must be last, uppercase after lowercase, fixed case sensitive natural compare +private fun naturalCompare(string1: String?, string2: String?): Int { + //noinspection StringEquality + if (string1 === string2) { + return 0 + } + if (string1 == null) { + return -1 + } + if (string2 == null) { + return 1 + } + + val string1Length = string1.length + val string2Length = string2.length + var i = 0 + var j = 0 + while (i < string1Length && j < string2Length) { + var ch1 = string1[i] + var ch2 = string2[j] + if ((StringUtil.isDecimalDigit(ch1) || ch1 == ' ') && (StringUtil.isDecimalDigit(ch2) || ch2 == ' ')) { + var startNum1 = i + while (ch1 == ' ' || ch1 == '0') { + // skip leading spaces and zeros + startNum1++ + if (startNum1 >= string1Length) { + break + } + ch1 = string1[startNum1] + } + var startNum2 = j + while (ch2 == ' ' || ch2 == '0') { + // skip leading spaces and zeros + startNum2++ + if (startNum2 >= string2Length) { + break + } + ch2 = string2[startNum2] + } + i = startNum1 + j = startNum2 + // find end index of number + while (i < string1Length && StringUtil.isDecimalDigit(string1[i])) { + i++ + } + while (j < string2Length && StringUtil.isDecimalDigit(string2[j])) { + j++ + } + val lengthDiff = (i - startNum1) - (j - startNum2) + if (lengthDiff != 0) { + // numbers with more digits are always greater than shorter numbers + return lengthDiff + } + while (startNum1 < i) { + // compare numbers with equal digit count + val diff = string1[startNum1] - string2[startNum2] + if (diff != 0) { + return diff + } + startNum1++ + startNum2++ + } + i-- + j-- + } + else if (ch1 != ch2) { + fun reverseCase(ch: Char) = when { + ch.isUpperCase() -> ch.toLowerCase() + ch.isLowerCase() -> ch.toUpperCase() + else -> ch + } + + when { + ch1 == '_' -> return 1 + ch2 == '_' -> return -1 + else -> return reverseCase(ch1) - reverseCase(ch2) + } + } + i++ + j++ + } + // After the loop the end of one of the strings might not have been reached, if the other + // string ends with a number and the strings are equal until the end of that number. When + // there are more characters in the string, then it is greater. + if (i < string1Length) { + return 1 + } + else if (j < string2Length) { + return -1 + } + return string1Length - string2Length +} + +@JvmOverloads fun createVariablesList(variables: List<Variable>, variableContext: VariableContext, memberFilter: MemberFilter? = null): XValueChildrenList { + return createVariablesList(variables, 0, variables.size, variableContext, memberFilter) +} + +fun createVariablesList(variables: List<Variable>, from: Int, to: Int, variableContext: VariableContext, memberFilter: MemberFilter?): XValueChildrenList { + val list = XValueChildrenList(to - from) + var getterOrSetterContext: VariableContext? = null + for (i in from until to) { + val variable = variables[i] + val normalizedName = memberFilter?.rawNameToSource(variable) ?: variable.name + list.add(VariableView(normalizedName, variable, variableContext)) + if (variable is ObjectProperty) { + if (variable.getter != null) { + if (getterOrSetterContext == null) { + getterOrSetterContext = NonWatchableVariableContext(variableContext) + } + list.add(VariableView(VariableImpl("get $normalizedName", variable.getter!!), getterOrSetterContext)) + } + if (variable.setter != null) { + if (getterOrSetterContext == null) { + getterOrSetterContext = NonWatchableVariableContext(variableContext) + } + list.add(VariableView(VariableImpl("set $normalizedName", variable.setter!!), getterOrSetterContext)) + } + } + } + return list +} + +private class NonWatchableVariableContext(variableContext: VariableContext) : VariableContextWrapper(variableContext, null) { + override fun watchableAsEvaluationExpression() = false +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/testSrc/Content.java b/platform/script-debugger/debugger-ui/testSrc/Content.java new file mode 100644 index 00000000..ff53d141 --- /dev/null +++ b/platform/script-debugger/debugger-ui/testSrc/Content.java @@ -0,0 +1,26 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger; + +import com.intellij.util.SmartList; + +import java.util.List; + +public final class Content { + public final List<TestCompositeNode> topGroups = new SmartList<>(); + public final List<TestValueNode> values = new SmartList<>(); + public final List<TestCompositeNode> bottomGroups = new SmartList<>(); +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/testSrc/TestCompositeNode.java b/platform/script-debugger/debugger-ui/testSrc/TestCompositeNode.java new file mode 100644 index 00000000..7886a05a --- /dev/null +++ b/platform/script-debugger/debugger-ui/testSrc/TestCompositeNode.java @@ -0,0 +1,133 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ +package org.jetbrains.debugger; + +import com.intellij.openapi.util.Condition; +import com.intellij.openapi.util.Conditions; +import com.intellij.ui.SimpleTextAttributes; +import com.intellij.util.Function; +import com.intellij.xdebugger.frame.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.concurrency.AsyncPromise; +import org.jetbrains.concurrency.Promise; +import org.jetbrains.concurrency.Promises; +import org.jetbrains.debugger.values.ObjectValue; + +import javax.swing.*; +import java.util.ArrayList; +import java.util.List; + +public class TestCompositeNode implements XCompositeNode { + private final AsyncPromise<XValueChildrenList> result = new AsyncPromise<>(); + private final XValueChildrenList children = new XValueChildrenList(); + + private final XValueGroup valueGroup; + public Content content; + + public TestCompositeNode() { + valueGroup = null; + } + + public TestCompositeNode(@NotNull XValueGroup group) { + valueGroup = group; + } + + @NotNull + public XValueGroup getValueGroup() { + return valueGroup; + } + + @Override + public void addChildren(@NotNull XValueChildrenList children, boolean last) { + for (XValueGroup group : children.getTopGroups()) { + this.children.addTopGroup(group); + } + for (int i = 0; i < children.size(); i++) { + this.children.add(children.getName(i), children.getValue(i)); + } + for (XValueGroup group : children.getBottomGroups()) { + this.children.addBottomGroup(group); + } + + if (last) { + result.setResult(this.children); + } + } + + @Override + public void tooManyChildren(int remaining) { + result.setResult(children); + } + + @Override + public void setAlreadySorted(boolean alreadySorted) { + } + + @Override + public void setErrorMessage(@NotNull String errorMessage) { + result.setError(errorMessage); + } + + @Override + public void setErrorMessage(@NotNull String errorMessage, @Nullable XDebuggerTreeNodeHyperlink link) { + setErrorMessage(errorMessage); + } + + @Override + public void setMessage(@NotNull String message, @Nullable Icon icon, @NotNull SimpleTextAttributes attributes, @Nullable XDebuggerTreeNodeHyperlink link) { + } + + @NotNull + public Promise<XValueChildrenList> getResult() { + return result; + } + + @NotNull + public Promise<Content> loadContent(@NotNull final Condition<XValueGroup> groupContentResolveCondition, @NotNull final Condition<VariableView> valueSubContentResolveCondition) { + assert content == null; + + content = new Content(); + return result.thenAsync(new Function<XValueChildrenList, Promise<Content>>() { + private void resolveGroups(@NotNull List<XValueGroup> valueGroups, @NotNull List<TestCompositeNode> resultNodes, @NotNull List<Promise<?>> promises) { + for (XValueGroup group : valueGroups) { + TestCompositeNode node = new TestCompositeNode(group); + boolean computeChildren = groupContentResolveCondition.value(group); + if (computeChildren) { + group.computeChildren(node); + } + resultNodes.add(node); + if (computeChildren) { + promises.add(node.loadContent(Conditions.alwaysFalse(), valueSubContentResolveCondition)); + } + } + } + + @NotNull + @Override + public Promise<Content> fun(XValueChildrenList list) { + List<Promise<?>> promises = new ArrayList<>(); + resolveGroups(children.getTopGroups(), content.topGroups, promises); + + for (int i = 0; i < children.size(); i++) { + XValue value = children.getValue(i); + TestValueNode node = new TestValueNode(); + node.myName = children.getName(i); + value.computePresentation(node, XValuePlace.TREE); + content.values.add(node); + promises.add(node.getResult()); + + // myHasChildren could be not computed yet + if (value instanceof VariableView && ((VariableView)value).getValue() instanceof ObjectValue && valueSubContentResolveCondition.value((VariableView)value)) { + promises.add(node.loadChildren(value)); + } + } + + resolveGroups(children.getBottomGroups(), content.bottomGroups, promises); + + return Promises.all(promises, content); + } + }); + } +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/testSrc/TestValueNode.java b/platform/script-debugger/debugger-ui/testSrc/TestValueNode.java new file mode 100644 index 00000000..85baf270 --- /dev/null +++ b/platform/script-debugger/debugger-ui/testSrc/TestValueNode.java @@ -0,0 +1,44 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger; + +import com.intellij.openapi.util.Conditions; +import com.intellij.xdebugger.XTestValueNode; +import com.intellij.xdebugger.frame.XValue; +import com.intellij.xdebugger.frame.presentation.XValuePresentation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.concurrency.AsyncPromise; +import org.jetbrains.concurrency.Promise; + +import javax.swing.*; + +public class TestValueNode extends XTestValueNode { + private final AsyncPromise<XTestValueNode> result = new AsyncPromise<>(); + + private volatile Content children; + + @NotNull + public Promise<XTestValueNode> getResult() { + return result; + } + + @NotNull + public Promise<Content> loadChildren(@NotNull XValue value) { + TestCompositeNode childrenNode = new TestCompositeNode(); + value.computeChildren(childrenNode); + return childrenNode.loadContent(Conditions.alwaysFalse(), Conditions.alwaysFalse()) + .onSuccess(content -> children = content); + } + + @Nullable + public Content getChildren() { + return children; + } + + @Override + public void applyPresentation(@Nullable Icon icon, @NotNull XValuePresentation valuePresentation, boolean hasChildren) { + super.applyPresentation(icon, valuePresentation, hasChildren); + + result.setResult(this); + } +}
\ No newline at end of file diff --git a/platform/script-debugger/debugger-ui/testSrc/testUtil.kt b/platform/script-debugger/debugger-ui/testSrc/testUtil.kt new file mode 100644 index 00000000..fc7d2f77 --- /dev/null +++ b/platform/script-debugger/debugger-ui/testSrc/testUtil.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.debugger + +import com.intellij.xdebugger.XDebugSession +import org.jetbrains.debugger.frame.CallFrameView + +val XDebugSession.topFrameView: CallFrameView + get() = currentStackFrame as CallFrameView
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-model-generator/generated/ProtocolSchemaReaderImpl.kt b/platform/script-debugger/protocol/protocol-model-generator/generated/ProtocolSchemaReaderImpl.kt new file mode 100644 index 00000000..352738cf --- /dev/null +++ b/platform/script-debugger/protocol/protocol-model-generator/generated/ProtocolSchemaReaderImpl.kt @@ -0,0 +1,355 @@ +// Generated source +package org.jetbrains.jsonProtocol + +import org.jetbrains.jsonProtocol.* + +import org.jetbrains.io.JsonReaderEx + +import org.jetbrains.jsonProtocol.JsonReaders.* + +internal class ProtocolSchemaReaderImpl : org.jetbrains.jsonProtocol.ProtocolSchemaReader { + override fun parseRoot(reader: org.jetbrains.io.JsonReaderEx): org.jetbrains.jsonProtocol.ProtocolMetaModel.Root = M0(reader, null) + + private class M0(reader: JsonReaderEx, preReadName: String?) : org.jetbrains.jsonProtocol.ProtocolMetaModel.Root { + override var version: org.jetbrains.jsonProtocol.ProtocolMetaModel.Version? = null + private var _domains: List<org.jetbrains.jsonProtocol.ProtocolMetaModel.Domain>? = null + + init { + var _n = preReadName + if (_n == null && reader.hasNext() && reader.beginObject().hasNext()) { + _n = reader.nextName() + } + + loop@ while (_n != null) { + when (_n) { + "version" -> version = M1(reader, null) + "domains" -> _domains = readObjectArray(reader, FM2()) + else -> reader.skipValue() + } + _n = reader.nextNameOrNull() + } + + reader.endObject() + } + + override fun domains() = _domains!! + + override fun equals(other: Any?): Boolean = other is M0 && version == other.version && _domains == other._domains + } + + private class M1(reader: JsonReaderEx, preReadName: String?) : org.jetbrains.jsonProtocol.ProtocolMetaModel.Version { + private var _major: String? = null + private var _minor: String? = null + + init { + var _n = preReadName + if (_n == null && reader.hasNext() && reader.beginObject().hasNext()) { + _n = reader.nextName() + } + + loop@ while (_n != null) { + when (_n) { + "major" -> _major = reader.nextString() + "minor" -> _minor = reader.nextString() + else -> reader.skipValue() + } + _n = reader.nextNameOrNull() + } + + reader.endObject() + } + + override fun major() = _major!! + + override fun minor() = _minor!! + + override fun equals(other: Any?): Boolean = other is M1 && _major == other._major && _minor == other._minor + } + + private class M2(reader: JsonReaderEx, preReadName: String?) : org.jetbrains.jsonProtocol.ProtocolMetaModel.Domain { + override var description: String? = null + override var events: List<org.jetbrains.jsonProtocol.ProtocolMetaModel.Event>? = null + override var experimental = false + override var hidden = false + override var types: List<org.jetbrains.jsonProtocol.ProtocolMetaModel.StandaloneType>? = null + private var _commands: List<org.jetbrains.jsonProtocol.ProtocolMetaModel.Command>? = null + private var _domain: String? = null + + init { + var _n = preReadName + if (_n == null && reader.hasNext() && reader.beginObject().hasNext()) { + _n = reader.nextName() + } + + loop@ while (_n != null) { + when (_n) { + "description" -> description = reader.nextNullableString() + "events" -> events = readObjectArray(reader, FM5()) + "experimental" -> experimental = reader.nextBoolean() + "hidden" -> hidden = reader.nextBoolean() + "types" -> types = readObjectArray(reader, FM6()) + "commands" -> _commands = readObjectArray(reader, FM3()) + "domain" -> _domain = reader.nextString() + else -> reader.skipValue() + } + _n = reader.nextNameOrNull() + } + + reader.endObject() + } + + override fun commands() = _commands!! + + override fun domain() = _domain!! + + override fun equals(other: Any?): Boolean = other is M2 && experimental == other.experimental && hidden == other.hidden && description == other.description && _domain == other._domain && events == other.events && types == other.types && _commands == other._commands + } + + private class M3(reader: JsonReaderEx, preReadName: String?) : org.jetbrains.jsonProtocol.ProtocolMetaModel.Command { + override var async = false + override var description: String? = null + override var hidden = false + override var parameters: List<org.jetbrains.jsonProtocol.ProtocolMetaModel.Parameter>? = null + override var returns: List<org.jetbrains.jsonProtocol.ProtocolMetaModel.Parameter>? = null + private var _name: String? = null + + init { + var _n = preReadName + if (_n == null && reader.hasNext() && reader.beginObject().hasNext()) { + _n = reader.nextName() + } + + loop@ while (_n != null) { + when (_n) { + "async" -> async = reader.nextBoolean() + "description" -> description = reader.nextNullableString() + "hidden" -> hidden = reader.nextBoolean() + "parameters" -> parameters = readObjectArray(reader, FM4()) + "returns" -> returns = readObjectArray(reader, FM4()) + "name" -> _name = reader.nextString() + else -> reader.skipValue() + } + _n = reader.nextNameOrNull() + } + + reader.endObject() + } + + override fun name() = _name!! + + override fun equals(other: Any?): Boolean = other is M3 && async == other.async && hidden == other.hidden && description == other.description && _name == other._name && parameters == other.parameters && returns == other.returns + } + + private class M4(reader: JsonReaderEx, preReadName: String?) : org.jetbrains.jsonProtocol.ProtocolMetaModel.Parameter { + override var default: String? = null + override var hidden = false + override var optional = false + override var shortName: String? = null + private var _name: String? = null + override var ref: String? = null + override var description: String? = null + override var enum: List<String>? = null + override var items: org.jetbrains.jsonProtocol.ProtocolMetaModel.ArrayItemType? = null + override var type: String? = null + + init { + var _n = preReadName + if (_n == null && reader.hasNext() && reader.beginObject().hasNext()) { + _n = reader.nextName() + } + + loop@ while (_n != null) { + when (_n) { + "default" -> default = readRawString(reader) + "hidden" -> hidden = reader.nextBoolean() + "optional" -> optional = reader.nextBoolean() + "shortName" -> shortName = reader.nextNullableString() + "name" -> _name = reader.nextString() + "\$ref" -> ref = reader.nextNullableString() + "description" -> description = reader.nextNullableString() + "enum" -> enum = nextList(reader) + "items" -> items = M7(reader, null) + "type" -> type = reader.nextNullableString() + else -> reader.skipValue() + } + _n = reader.nextNameOrNull() + } + + reader.endObject() + } + + override fun name() = _name!! + + override fun equals(other: Any?): Boolean = other is M4 && hidden == other.hidden && optional == other.optional && default == other.default && shortName == other.shortName && _name == other._name && ref == other.ref && description == other.description && type == other.type && enum == other.enum && items == other.items + } + + private class M5(reader: JsonReaderEx, preReadName: String?) : org.jetbrains.jsonProtocol.ProtocolMetaModel.Event { + override var description: String? = null + override var hidden = false + override var optionalData = false + override var parameters: List<org.jetbrains.jsonProtocol.ProtocolMetaModel.Parameter>? = null + private var _name: String? = null + + init { + var _n = preReadName + if (_n == null && reader.hasNext() && reader.beginObject().hasNext()) { + _n = reader.nextName() + } + + loop@ while (_n != null) { + when (_n) { + "description" -> description = reader.nextNullableString() + "hidden" -> hidden = reader.nextBoolean() + "optionalData" -> optionalData = reader.nextBoolean() + "parameters" -> parameters = readObjectArray(reader, FM4()) + "name" -> _name = reader.nextString() + else -> reader.skipValue() + } + _n = reader.nextNameOrNull() + } + + reader.endObject() + } + + override fun name() = _name!! + + override fun equals(other: Any?): Boolean = other is M5 && hidden == other.hidden && optionalData == other.optionalData && description == other.description && _name == other._name && parameters == other.parameters + } + + private class M6(reader: JsonReaderEx, preReadName: String?) : org.jetbrains.jsonProtocol.ProtocolMetaModel.StandaloneType { + override var hidden = false + private var _id: String? = null + override var properties: List<org.jetbrains.jsonProtocol.ProtocolMetaModel.ObjectProperty>? = null + override var description: String? = null + override var enum: List<String>? = null + override var items: org.jetbrains.jsonProtocol.ProtocolMetaModel.ArrayItemType? = null + override var type: String? = null + + init { + var _n = preReadName + if (_n == null && reader.hasNext() && reader.beginObject().hasNext()) { + _n = reader.nextName() + } + + loop@ while (_n != null) { + when (_n) { + "hidden" -> hidden = reader.nextBoolean() + "id" -> _id = reader.nextString() + "properties" -> properties = readObjectArray(reader, FM8()) + "description" -> description = reader.nextNullableString() + "enum" -> enum = nextList(reader) + "items" -> items = M7(reader, null) + "type" -> type = reader.nextNullableString() + else -> reader.skipValue() + } + _n = reader.nextNameOrNull() + } + + reader.endObject() + } + + override fun id() = _id!! + + override fun equals(other: Any?): Boolean = other is M6 && hidden == other.hidden && _id == other._id && description == other.description && type == other.type && properties == other.properties && enum == other.enum && items == other.items + } + + private class M7(reader: JsonReaderEx, preReadName: String?) : org.jetbrains.jsonProtocol.ProtocolMetaModel.ArrayItemType { + override var optional = false + override var properties: List<org.jetbrains.jsonProtocol.ProtocolMetaModel.ObjectProperty>? = null + override var description: String? = null + override var enum: List<String>? = null + override var items: org.jetbrains.jsonProtocol.ProtocolMetaModel.ArrayItemType? = null + override var type: String? = null + override var ref: String? = null + + init { + var _n = preReadName + if (_n == null && reader.hasNext() && reader.beginObject().hasNext()) { + _n = reader.nextName() + } + + loop@ while (_n != null) { + when (_n) { + "optional" -> optional = reader.nextBoolean() + "properties" -> properties = readObjectArray(reader, FM8()) + "description" -> description = reader.nextNullableString() + "enum" -> enum = nextList(reader) + "items" -> items = M7(reader, null) + "type" -> type = reader.nextNullableString() + "\$ref" -> ref = reader.nextNullableString() + else -> reader.skipValue() + } + _n = reader.nextNameOrNull() + } + + reader.endObject() + } + + override fun equals(other: Any?): Boolean = other is M7 && optional == other.optional && description == other.description && type == other.type && ref == other.ref && properties == other.properties && enum == other.enum && items == other.items + } + + private class M8(reader: JsonReaderEx, preReadName: String?) : org.jetbrains.jsonProtocol.ProtocolMetaModel.ObjectProperty { + override var hidden = false + private var _name: String? = null + override var optional = false + override var shortName: String? = null + override var ref: String? = null + override var description: String? = null + override var enum: List<String>? = null + override var items: org.jetbrains.jsonProtocol.ProtocolMetaModel.ArrayItemType? = null + override var type: String? = null + + init { + var _n = preReadName + if (_n == null && reader.hasNext() && reader.beginObject().hasNext()) { + _n = reader.nextName() + } + + loop@ while (_n != null) { + when (_n) { + "hidden" -> hidden = reader.nextBoolean() + "name" -> _name = reader.nextString() + "optional" -> optional = reader.nextBoolean() + "shortName" -> shortName = reader.nextNullableString() + "\$ref" -> ref = reader.nextNullableString() + "description" -> description = reader.nextNullableString() + "enum" -> enum = nextList(reader) + "items" -> items = M7(reader, null) + "type" -> type = reader.nextNullableString() + else -> reader.skipValue() + } + _n = reader.nextNameOrNull() + } + + reader.endObject() + } + + override fun name() = _name!! + + override fun equals(other: Any?): Boolean = other is M8 && hidden == other.hidden && optional == other.optional && _name == other._name && shortName == other.shortName && ref == other.ref && description == other.description && type == other.type && enum == other.enum && items == other.items + } + + private class FM2 : ObjectFactory<org.jetbrains.jsonProtocol.ProtocolMetaModel.Domain>() { + override fun read(reader: JsonReaderEx): org.jetbrains.jsonProtocol.ProtocolMetaModel.Domain = M2(reader, null) + } + + private class FM5 : ObjectFactory<org.jetbrains.jsonProtocol.ProtocolMetaModel.Event>() { + override fun read(reader: JsonReaderEx): org.jetbrains.jsonProtocol.ProtocolMetaModel.Event = M5(reader, null) + } + + private class FM6 : ObjectFactory<org.jetbrains.jsonProtocol.ProtocolMetaModel.StandaloneType>() { + override fun read(reader: JsonReaderEx): org.jetbrains.jsonProtocol.ProtocolMetaModel.StandaloneType = M6(reader, null) + } + + private class FM3 : ObjectFactory<org.jetbrains.jsonProtocol.ProtocolMetaModel.Command>() { + override fun read(reader: JsonReaderEx): org.jetbrains.jsonProtocol.ProtocolMetaModel.Command = M3(reader, null) + } + + private class FM4 : ObjectFactory<org.jetbrains.jsonProtocol.ProtocolMetaModel.Parameter>() { + override fun read(reader: JsonReaderEx): org.jetbrains.jsonProtocol.ProtocolMetaModel.Parameter = M4(reader, null) + } + + private class FM8 : ObjectFactory<org.jetbrains.jsonProtocol.ProtocolMetaModel.ObjectProperty>() { + override fun read(reader: JsonReaderEx): org.jetbrains.jsonProtocol.ProtocolMetaModel.ObjectProperty = M8(reader, null) + } +}
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-model-generator/intellij.javascript.protocolModelGenerator.iml b/platform/script-debugger/protocol/protocol-model-generator/intellij.javascript.protocolModelGenerator.iml new file mode 100644 index 00000000..8b9e75fb --- /dev/null +++ b/platform/script-debugger/protocol/protocol-model-generator/intellij.javascript.protocolModelGenerator.iml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" packagePrefix="org.jetbrains.protocolModelGenerator" /> + <sourceFolder url="file://$MODULE_DIR$/generated" isTestSource="false" generated="true" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + <orderEntry type="library" name="gson" level="project" /> + <orderEntry type="module" module-name="intellij.javascript.schemaReaderGenerator" /> + <orderEntry type="module" module-name="intellij.javascript.protocolReader" /> + <orderEntry type="library" name="Trove4j" level="project" /> + <orderEntry type="module" module-name="intellij.platform.util.rt" /> + <orderEntry type="module" module-name="intellij.platform.ide.impl" /> + <orderEntry type="library" name="KotlinJavaRuntime" level="project" /> + </component> +</module>
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-model-generator/src/BoxableType.kt b/platform/script-debugger/protocol/protocol-model-generator/src/BoxableType.kt new file mode 100644 index 00000000..8fda9d35 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-model-generator/src/BoxableType.kt @@ -0,0 +1,21 @@ +package org.jetbrains.protocolModelGenerator + +interface BoxableType { + val defaultValue: String? + + val fullText: CharSequence + + fun getShortText(contextNamespace: NamePath): String + + val writeMethodName: String + + companion object { + val STRING: StandaloneType = StandaloneType(NamePath("String"), "writeString", "null") + val ANY_STRING: StandaloneType = StandaloneType(NamePath("String"), "writeString", "null") + val INT: StandaloneType = StandaloneType(NamePath("Int"), "writeInt", null) + val LONG: StandaloneType = StandaloneType(NamePath("Long"), "writeLong", null) + val NUMBER: StandaloneType = StandaloneType(NamePath("Double"), "writeDouble", "null") + val BOOLEAN: StandaloneType = StandaloneType(NamePath("Boolean"), "writeBoolean", null) + val MAP: StandaloneType = StandaloneType(NamePath("Map<String, String>"), "writeMap", "null") + } +}
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-model-generator/src/ClassNameScheme.kt b/platform/script-debugger/protocol/protocol-model-generator/src/ClassNameScheme.kt new file mode 100644 index 00000000..64571610 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-model-generator/src/ClassNameScheme.kt @@ -0,0 +1,48 @@ +package org.jetbrains.protocolModelGenerator + +fun getPackageName(rootPackage: String, domain: String): String { + if (domain.isEmpty()) { + return rootPackage + } + return "$rootPackage.${domain.toLowerCase()}" +} + +abstract class ClassNameScheme(private val suffix: String, private val rootPackage: String) { + fun getFullName(domainName: String, baseName: String): NamePath { + return NamePath(getShortName(baseName), NamePath(getPackageNameVirtual(domainName))) + } + + fun getShortName(baseName: String): String { + return if (baseName.endsWith("Descriptor")) baseName else String(getShortNameChars(baseName)) + } + + private fun getShortNameChars(baseName: String): CharArray { + val name = CharArray(baseName.length + suffix.length) + baseName.toCharArray(name, 0, 0, baseName.length) + if (!suffix.isEmpty()) { + suffix.toCharArray(name, baseName.length, 0, suffix.length) + } + if (Character.isLowerCase(name[0])) { + name[0] = Character.toUpperCase(name[0]) + } + if (baseName.endsWith("breakpoint")) { + name[baseName.length - "breakpoint".length] = 'B' + } + else if (baseName.endsWith("breakpoints")) { + name[baseName.length - "breakpoints".length] = 'B' + } + return name + } + + fun getPackageNameVirtual(domainName: String): String = getPackageName(rootPackage, domainName) + + class Input(suffix: String, rootPackage: String) : ClassNameScheme(suffix, rootPackage) { + fun getParseMethodName(domain: String, name: String): String { + return "read" + capitalizeFirstChar(domain) + getShortName(name) + } + } + + class Output(suffix: String, rootPackage: String) : ClassNameScheme(suffix, rootPackage) + + class Common(suffix: String, rootPackage: String) : ClassNameScheme(suffix, rootPackage) +}
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-model-generator/src/ClassScope.kt b/platform/script-debugger/protocol/protocol-model-generator/src/ClassScope.kt new file mode 100644 index 00000000..74c89b5f --- /dev/null +++ b/platform/script-debugger/protocol/protocol-model-generator/src/ClassScope.kt @@ -0,0 +1,28 @@ +package org.jetbrains.protocolModelGenerator + +import com.intellij.util.SmartList +import org.jetbrains.jsonProtocol.ItemDescriptor +import org.jetbrains.protocolReader.TextOutput + +internal abstract class ClassScope(val generator: DomainGenerator, val classContextNamespace: NamePath) { + private val additionalMemberTexts = SmartList<(out: TextOutput) -> Unit>() + + fun addMember(appender: (out: TextOutput) -> Unit) { + additionalMemberTexts.add(appender) + } + + fun writeAdditionalMembers(out: TextOutput) { + if (additionalMemberTexts.isEmpty()) { + return + } + + out.newLine() + for (deferredWriter in additionalMemberTexts) { + deferredWriter(out) + } + } + + abstract val typeDirection: TypeData.Direction +} + +fun ItemDescriptor.Named.getName(): String = shortName ?: name()
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-model-generator/src/CreateStandaloneTypeBindingVisitorBase.kt b/platform/script-debugger/protocol/protocol-model-generator/src/CreateStandaloneTypeBindingVisitorBase.kt new file mode 100644 index 00000000..15168456 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-model-generator/src/CreateStandaloneTypeBindingVisitorBase.kt @@ -0,0 +1,27 @@ +package org.jetbrains.protocolModelGenerator + +import org.jetbrains.jsonProtocol.ProtocolMetaModel + +internal abstract class CreateStandaloneTypeBindingVisitorBase(private val generator: DomainGenerator, protected val type: ProtocolMetaModel.StandaloneType) : TypeVisitor<StandaloneTypeBinding> { + override fun visitString(): StandaloneTypeBinding { + return generator.createTypedefTypeBinding(type, PredefinedTarget.STRING, generator.generator.naming.commonTypedef, null) + } + + override fun visitInteger() = generator.createTypedefTypeBinding(type, PredefinedTarget.INT, generator.generator.naming.commonTypedef, null) + + override fun visitRef(refName: String) = throw RuntimeException() + + override fun visitBoolean() = throw RuntimeException() + + override fun visitNumber(): StandaloneTypeBinding { + return generator.createTypedefTypeBinding(type, PredefinedTarget.NUMBER, generator.generator.naming.commonTypedef, null) + } + + override fun visitMap(): StandaloneTypeBinding { + return generator.createTypedefTypeBinding(type, PredefinedTarget.MAP, generator.generator.naming.commonTypedef, null) + } + + override fun visitUnknown(): StandaloneTypeBinding { + throw RuntimeException() + } +} diff --git a/platform/script-debugger/protocol/protocol-model-generator/src/DomainGenerator.kt b/platform/script-debugger/protocol/protocol-model-generator/src/DomainGenerator.kt new file mode 100644 index 00000000..b5ff9814 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-model-generator/src/DomainGenerator.kt @@ -0,0 +1,289 @@ +package org.jetbrains.protocolModelGenerator + +import com.intellij.util.SmartList +import com.intellij.util.containers.isNullOrEmpty +import org.jetbrains.jsonProtocol.ItemDescriptor +import org.jetbrains.jsonProtocol.ProtocolMetaModel +import org.jetbrains.protocolReader.FileUpdater +import org.jetbrains.protocolReader.JSON_READER_PARAMETER_DEF +import org.jetbrains.protocolReader.TextOutput +import org.jetbrains.protocolReader.appendEnums + +internal class DomainGenerator(val generator: Generator, val domain: ProtocolMetaModel.Domain, val fileUpdater: FileUpdater) { + fun registerTypes() { + domain.types?.let { + for (type in it) { + generator.typeMap.getTypeData(domain.domain(), type.id()).type = type + } + } + } + + fun generateCommandsAndEvents() { + val commands = domain.commands().sortedBy(ProtocolMetaModel.Command::name) + for (command in commands) { + val hasResponse = command.returns != null + val returnType = if (hasResponse) generator.naming.commandResult.getShortName(command.name()) else "Unit" + generateTopLevelOutputClass(generator.naming.params, command.name(), command.description, "${generator.naming.requestClassName}<$returnType>", { + append('"') + if (!domain.domain().isEmpty()) { + append(domain.domain()).append('.') + } + append(command.name()).append('"') + }, command.parameters) + + if (hasResponse) { + generateJsonProtocolInterface(generator.naming.commandResult.getShortName(command.name()), command.description, command.returns, null) + generator.jsonProtocolParserClassNames.add(generator.naming.commandResult.getFullName(domain.domain(), command.name()).getFullText()) + generator.parserRootInterfaceItems.add(ParserRootInterfaceItem(domain.domain(), command.name(), generator.naming.commandResult)) + } + } + + if (domain.events != null) { + val events = domain.events!!.sortedBy(ProtocolMetaModel.Event::name) + for (event in events) { + generateEvenData(event) + generator.jsonProtocolParserClassNames.add(generator.naming.eventData.getFullName(domain.domain(), event.name()).getFullText()) + generator.parserRootInterfaceItems.add(ParserRootInterfaceItem(domain.domain(), event.name(), generator.naming.eventData)) + } + } + } + + fun generateCommandAdditionalParam(type: ProtocolMetaModel.StandaloneType) { + generateTopLevelOutputClass(generator.naming.additionalParam, type.id(), type.description, null, null, type.properties) + } + + private fun <P : ItemDescriptor.Named> generateTopLevelOutputClass(nameScheme: ClassNameScheme, baseName: String, description: String?, baseType: String?, methodName: (TextOutput.() -> Unit)?, properties: List<P>?) { + generateOutputClass(fileUpdater.out.newLine().newLine(), nameScheme.getFullName(domain.domain(), baseName), description, baseType, methodName, properties) + } + + private fun <P : ItemDescriptor.Named> generateOutputClass(out: TextOutput, classNamePath: NamePath, description: String?, baseType: String?, methodName: (TextOutput.() -> Unit)?, properties: List<P>?) { + out.doc(description) + + out.append(if (baseType == null) "class" else "fun").space().append(classNamePath.lastComponent).append('(') + + val classScope = OutputClassScope(this, classNamePath) + val (mandatoryParameters, optionalParameters) = getParametersInfo(classScope, properties) + if (properties.isNullOrEmpty()) { + assert(baseType != null) + + out.append(") = ") + out.append(baseType ?: "org.jetbrains.jsonProtocol.OutMessage") + out.append('(') + methodName?.invoke(out) + out.append(')') + return + } + else { + classScope.writeMethodParameters(out, mandatoryParameters, false) + classScope.writeMethodParameters(out, optionalParameters, mandatoryParameters.isNotEmpty()) + + out.append(')') + out.append(" : ").append(baseType ?: "org.jetbrains.jsonProtocol.OutMessage") + if (baseType == null) { + out.append("()").openBlock().append("init") + } + } + + out.block(baseType != null) { + if (baseType != null) { + out.append("val m = ").append(baseType) + out.append('(') + methodName?.invoke(out) + out.append(')') + } + + if (!properties.isNullOrEmpty()) { + val qualifier = if (baseType == null) null else "m" + classScope.writeWriteCalls(out, mandatoryParameters, qualifier) + classScope.writeWriteCalls(out, optionalParameters, qualifier) + if (baseType != null) { + out.newLine().append("return m") + } + } + } + + if (baseType == null) { + // close class + out.closeBlock() + } + + classScope.writeAdditionalMembers(out) + } + + private fun <P : ItemDescriptor.Named> getParametersInfo(classScope: OutputClassScope, properties: List<P>?): Pair<List<Pair<P, BoxableType>>, List<Pair<P, BoxableType>>> { + if (properties.isNullOrEmpty()) { + return Pair(emptyList(), emptyList()) + } + + val mandatoryParameters = SmartList<Pair<P, BoxableType>>() + val optionalParameters = SmartList<Pair<P, BoxableType>>() + if (properties != null) { + for (parameter in properties) { + val type = MemberScope(classScope, parameter.name()).resolveType(parameter).type + if (parameter.optional) { + optionalParameters.add(parameter to type) + } + else { + mandatoryParameters.add(parameter to type) + } + } + } + return Pair(mandatoryParameters, optionalParameters) + } + + fun createStandaloneOutputTypeBinding(type: ProtocolMetaModel.StandaloneType, name: String) = switchByType(type, MyCreateStandaloneTypeBindingVisitorBase(this, type, name)) + + fun createStandaloneInputTypeBinding(type: ProtocolMetaModel.StandaloneType): StandaloneTypeBinding { + return switchByType(type, object : CreateStandaloneTypeBindingVisitorBase(this, type) { + override fun visitObject(properties: List<ProtocolMetaModel.ObjectProperty>?) = createStandaloneObjectInputTypeBinding(type, properties) + + override fun visitEnum(enumConstants: List<String>): StandaloneTypeBinding { + val name = type.id() + return object : StandaloneTypeBinding { + override fun getJavaType() = StandaloneType(generator.naming.inputEnum.getFullName(domain.domain(), name), "writeEnum") + + override fun generate() { + fileUpdater.out.doc(type.description) + appendEnums(enumConstants, generator.naming.inputEnum.getShortName(name), true, fileUpdater.out) + } + + override fun getDirection() = TypeData.Direction.INPUT + } + } + + override fun visitArray(items: ProtocolMetaModel.ArrayItemType): StandaloneTypeBinding { + val resolveAndGenerateScope = object : ResolveAndGenerateScope { + // This class is responsible for generating ad hoc type. + // If we ever are to do it, we should generate into string buffer and put strings + // inside TypeDef class. + override fun getDomainName() = domain.domain() + + override fun getTypeDirection() = TypeData.Direction.INPUT + + override fun generateNestedObject(description: String?, properties: List<ProtocolMetaModel.ObjectProperty>?) = throw UnsupportedOperationException() + } + + val arrayType = ListType(generator.resolveType(items, resolveAndGenerateScope).type) + return createTypedefTypeBinding(type, object : Target { + override fun resolve(context: Target.ResolveContext) = arrayType + }, generator.naming.inputTypedef, TypeData.Direction.INPUT) + } + }) + } + + fun createStandaloneObjectInputTypeBinding(type: ProtocolMetaModel.StandaloneType, properties: List<ProtocolMetaModel.ObjectProperty>?): StandaloneTypeBinding { + val name = type.id() + val fullTypeName = generator.naming.inputValue.getFullName(domain.domain(), name) + generator.jsonProtocolParserClassNames.add(fullTypeName.getFullText()) + + return object : StandaloneTypeBinding { + override fun getJavaType() = subMessageType(fullTypeName) + + override fun generate() { + val className = generator.naming.inputValue.getFullName(domain.domain(), name) + val out = fileUpdater.out + out.newLine().newLine() + descriptionAndRequiredImport(type.description, out) + + out.append("interface ").append(className.lastComponent).openBlock() + val classScope = InputClassScope(this@DomainGenerator, className) + if (properties != null) { + classScope.generateDeclarationBody(out, properties) + } + classScope.writeAdditionalMembers(out) + out.closeBlock() + } + + override fun getDirection() = TypeData.Direction.INPUT + } + } + + /** + * Typedef is an empty class that just holds description and + * refers to an actual type (such as String). + */ + fun createTypedefTypeBinding(type: ProtocolMetaModel.StandaloneType, target: Target, nameScheme: ClassNameScheme, direction: TypeData.Direction?): StandaloneTypeBinding { + val name = type.id() + val typedefJavaName = nameScheme.getFullName(domain.domain(), name) + val actualJavaType = target.resolve(object : Target.ResolveContext { + override fun generateNestedObject(shortName: String, description: String?, properties: List<ProtocolMetaModel.ObjectProperty>?): BoxableType { + if (direction == null) { + throw RuntimeException("Unsupported") + } + + when (direction) { + TypeData.Direction.INPUT -> throw RuntimeException("TODO") + TypeData.Direction.OUTPUT -> generateOutputClass(TextOutput(StringBuilder()), NamePath(shortName, typedefJavaName), description, null, null, properties) + } + return subMessageType(NamePath(shortName, typedefJavaName)) + } + }) + + return object : StandaloneTypeBinding { + override fun getJavaType() = actualJavaType + + override fun generate() { + } + + override fun getDirection() = direction + } + } + + private fun generateEvenData(event: ProtocolMetaModel.Event) { + val className = generator.naming.eventData.getShortName(event.name()) + val domainName = domain.domain() + val fullName = generator.naming.eventData.getFullName(domainName, event.name()).getFullText() + generateJsonProtocolInterface(className, event.description, event.parameters) { out -> + out.newLine().append("companion object TYPE : org.jetbrains.jsonProtocol.EventType<").append(fullName) + if (event.optionalData || event.parameters.isNullOrEmpty()) { + out.append('?') + } + out.append(", ").append(generator.naming.inputPackage).append('.').append(READER_INTERFACE_NAME).append('>') + out.append("(\"") + if (!domainName.isNullOrEmpty()) { + out.append(domainName).append('.') + } + out.append(event.name()).append("\")").block() { + out.append("override fun read(protocolReader: ") + out.append(generator.naming.inputPackage).append('.').append(READER_INTERFACE_NAME).append(", ").append(JSON_READER_PARAMETER_DEF).append(")") + out.append(" = protocolReader.").append(generator.naming.eventData.getParseMethodName(domainName, event.name())).append("(reader)") + } + } + } + + private fun generateJsonProtocolInterface(className: String, description: String?, parameters: List<ProtocolMetaModel.Parameter>?, additionalMembersText: ((out: TextOutput) -> Unit)?) { + val out = fileUpdater.out + out.newLine().newLine() + descriptionAndRequiredImport(description, out) + + var hasNodeId = false + val parametersToAdd = parameters?.filter(fun(p: ProtocolMetaModel.Parameter): Boolean { + if (p.name() == "nodeId" && p.ref == "NodeId") { + hasNodeId = true + return false + } + else return true + }) + out.append("interface ").append(className) + if (hasNodeId) out.append(" : ").append("org.jetbrains.wip.protocol.NodeIdentifiable") + out.block { + val classScope = InputClassScope(this, NamePath(className, NamePath(getPackageName(generator.naming.inputPackage, domain.domain())))) + if (additionalMembersText != null) { + classScope.addMember(additionalMembersText) + } + if (parametersToAdd != null) { + classScope.generateDeclarationBody(out, parametersToAdd) + } + classScope.writeAdditionalMembers(out) + } + } + + private fun descriptionAndRequiredImport(description: String?, out: TextOutput) { + if (description != null) { + out.doc(description) + } +// out.append("@JsonType").newLine() + } +} + +fun subMessageType(namePath: NamePath): StandaloneType = StandaloneType(namePath, "writeMessage", null)
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-model-generator/src/Enums.kt b/platform/script-debugger/protocol/protocol-model-generator/src/Enums.kt new file mode 100644 index 00000000..5cfaec18 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-model-generator/src/Enums.kt @@ -0,0 +1,33 @@ +package org.jetbrains.protocolReader + +import org.jetbrains.jsonProtocol.JsonReaders + +internal fun appendEnums(enumConstants: List<String>, enumName: String, input: Boolean, out: TextOutput) { + out.append("enum class ").append(enumName) + + if (!input) { + out.append("(private val protocolValue: String)") + } + + out.openBlock() + for (constant in enumConstants) { + out.append(JsonReaders.convertRawEnumName(constant)) + if (!input) { + out.append("(\"").append(constant).append("\")") + if (enumConstants.get(enumConstants.size - 1) != constant) { + out.comma() + } + } + else { + out.comma() + } + } + + if (input) { + out.append("NO_ENUM_CONST") + } + else { + out.append(';').newLine().newLine().append("override fun toString() = protocolValue") + } + out.closeBlock() +}
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-model-generator/src/FileSet.kt b/platform/script-debugger/protocol/protocol-model-generator/src/FileSet.kt new file mode 100644 index 00000000..a79acfde --- /dev/null +++ b/platform/script-debugger/protocol/protocol-model-generator/src/FileSet.kt @@ -0,0 +1,52 @@ +package org.jetbrains.protocolModelGenerator + +import gnu.trove.THashSet +import gnu.trove.TObjectProcedure +import org.jetbrains.protocolReader.FileUpdater +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.SimpleFileVisitor +import java.nio.file.attribute.BasicFileAttributes + +/** + * Records a list of files in the root directory and deletes files that were not re-generated. + */ +class FileSet(private val rootDir: Path) { + private val unusedFiles = THashSet<Path>() + + init { + Files.walkFileTree(rootDir, object : SimpleFileVisitor<Path>() { + override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult { + return if (Files.isHidden(dir)) FileVisitResult.SKIP_SUBTREE else FileVisitResult.CONTINUE + } + + override fun visitFile(path: Path, attrs: BasicFileAttributes): FileVisitResult { + if (!Files.isHidden(path)) { + unusedFiles.add(path) + } + return FileVisitResult.CONTINUE + } + }) + } + + fun createFileUpdater(filePath: String): FileUpdater { + val file = rootDir.resolve(filePath) + unusedFiles.remove(file) + return FileUpdater(file) + } + + fun deleteOtherFiles() { + unusedFiles.forEach(TObjectProcedure<Path> { it -> + if (Files.deleteIfExists(it)) { + val parent = it.parent + Files.newDirectoryStream(parent).use { stream -> + if (!stream.iterator().hasNext()) { + Files.delete(parent) + } + } + } + true + }) + } +} diff --git a/platform/script-debugger/protocol/protocol-model-generator/src/Generator.kt b/platform/script-debugger/protocol/protocol-model-generator/src/Generator.kt new file mode 100644 index 00000000..b93c45fa --- /dev/null +++ b/platform/script-debugger/protocol/protocol-model-generator/src/Generator.kt @@ -0,0 +1,275 @@ +package org.jetbrains.protocolModelGenerator + +import com.intellij.openapi.util.text.StringUtil +import com.intellij.util.containers.ContainerUtil +import com.intellij.util.containers.isNullOrEmpty +import gnu.trove.THashMap +import org.jetbrains.io.JsonReaderEx +import org.jetbrains.jsonProtocol.* +import org.jetbrains.protocolReader.TextOutput +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.net.URL +import java.nio.file.FileSystems +import java.nio.file.Files +import java.util.* + +fun main(args: Array<String>) { + val outputDir = args[0] + val roots = IntRange(3, args.size - 1).map { + val schemaUrl = args[it] + val bytes: ByteArray + if (schemaUrl.startsWith("http")) { + bytes = loadBytes(URL(schemaUrl).openStream()) + } + else { + bytes = Files.readAllBytes(FileSystems.getDefault().getPath(schemaUrl)) + } + val reader = JsonReaderEx(bytes.toString(Charsets.UTF_8)) + reader.isLenient = true + ProtocolSchemaReaderImpl().parseRoot(reader) + } + val mergedRoot = if (roots.size == 1) roots[0] else object : ProtocolMetaModel.Root { + override val version: ProtocolMetaModel.Version? + get() = roots[0].version + + override fun domains(): List<ProtocolMetaModel.Domain> { + return ContainerUtil.concat(roots.map { it.domains() }) + } + } + Generator(outputDir, args[1], args[2], mergedRoot) +} + +private fun loadBytes(stream: InputStream): ByteArray { + val buffer = ByteArrayOutputStream(Math.max(stream.available(), 16 * 1024)) + val bytes = ByteArray(1024 * 20) + while (true) { + val n = stream.read(bytes, 0, bytes.size) + if (n <= 0) { + break + } + buffer.write(bytes, 0, n) + } + buffer.close() + return buffer.toByteArray() +} + +internal class Naming(val inputPackage: String, val requestClassName: String) { + val params = ClassNameScheme.Output("", inputPackage) + val additionalParam = ClassNameScheme.Output("", inputPackage) + val outputTypedef: ClassNameScheme = ClassNameScheme.Output("Typedef", inputPackage) + + val commandResult = ClassNameScheme.Input("Result", inputPackage) + val eventData = ClassNameScheme.Input("EventData", inputPackage) + val inputValue = ClassNameScheme.Input("Value", inputPackage) + val inputEnum = ClassNameScheme.Input("", inputPackage) + val inputTypedef = ClassNameScheme.Input("Typedef", inputPackage) + + val commonTypedef = ClassNameScheme.Common("Typedef", inputPackage) +} + +/** + * Read metamodel and generates set of files with Java classes/interfaces for the protocol. + */ +internal class Generator(outputDir: String, private val rootPackage: String, requestClassName: String, metamodel: ProtocolMetaModel.Root) { + val jsonProtocolParserClassNames = ArrayList<String>() + val parserRootInterfaceItems = ArrayList<ParserRootInterfaceItem>() + val typeMap = TypeMap() + + val nestedTypeMap = THashMap<NamePath, StandaloneType>() + + val fileSet = FileSet(FileSystems.getDefault().getPath(outputDir)) + val naming = Naming(rootPackage, requestClassName) + + init { + val domainList = metamodel.domains() + val domainGeneratorMap = THashMap<String, DomainGenerator>() + + for (domain in domainList) { + if (!INCLUDED_DOMAINS.contains(domain.domain())) { + System.out.println("Domain skipped: ${domain.domain()}") + continue + } + + val fileUpdater = fileSet.createFileUpdater("${StringUtil.nullize(domain.domain()) ?: "protocol"}.kt") + val out = fileUpdater.out + + out.append("// Generated source").newLine().append("package ").append(getPackageName(rootPackage, domain.domain())).newLine().newLine() + out.append("import org.jetbrains.jsonProtocol.*").newLine() + out.append("import org.jetbrains.io.JsonReaderEx").newLine() + + val domainGenerator = DomainGenerator(this, domain, fileUpdater) + domainGeneratorMap.put(domain.domain(), domainGenerator) + domainGenerator.registerTypes() + + out.newLine() + + System.out.println("Domain generated: ${domain.domain()}") + } + + typeMap.domainGeneratorMap = domainGeneratorMap + + for (domainGenerator in domainGeneratorMap.values) { + domainGenerator.generateCommandsAndEvents() + } + + val sharedFileUpdater = if (domainGeneratorMap.size == 1) { + domainGeneratorMap.values.first().fileUpdater + } + else { + val fileUpdater = fileSet.createFileUpdater("protocol.kt") + val out = fileUpdater.out + out.append("// Generated source").newLine().append("package ").append(rootPackage).newLine().newLine() + out.append("import org.jetbrains.jsonProtocol.*").newLine() + out.append("import org.jetbrains.io.JsonReaderEx").newLine() + fileUpdater + } + typeMap.generateRequestedTypes() + generateParserInterfaceList(sharedFileUpdater.out) + generateParserRoot(parserRootInterfaceItems, sharedFileUpdater.out) + fileSet.deleteOtherFiles() + + for (domainGenerator in domainGeneratorMap.values) { + domainGenerator.fileUpdater.update() + } + + if (domainGeneratorMap.size != 1) { + sharedFileUpdater.update() + } + } + + fun resolveType(itemDescriptor: ItemDescriptor, scope: ResolveAndGenerateScope): TypeDescriptor { + return switchByType(itemDescriptor, object : TypeVisitor<TypeDescriptor> { + override fun visitRef(refName: String) = TypeDescriptor(resolveRefType(scope.getDomainName(), refName, scope.getTypeDirection()), itemDescriptor) + + override fun visitBoolean() = TypeDescriptor(BoxableType.BOOLEAN, itemDescriptor) + + override fun visitEnum(enumConstants: List<String>): TypeDescriptor { + assert(scope is MemberScope) + return TypeDescriptor((scope as MemberScope).generateEnum(itemDescriptor.description, enumConstants), itemDescriptor) + } + + override fun visitString() = TypeDescriptor(BoxableType.STRING, itemDescriptor) + + override fun visitInteger() = TypeDescriptor(BoxableType.INT, itemDescriptor) + + override fun visitNumber() = TypeDescriptor(BoxableType.NUMBER, itemDescriptor) + + override fun visitMap() = TypeDescriptor(BoxableType.MAP, itemDescriptor) + + override fun visitArray(items: ProtocolMetaModel.ArrayItemType): TypeDescriptor { + val type = scope.resolveType(items).type + return TypeDescriptor(ListType(type), itemDescriptor, type == BoxableType.ANY_STRING) + } + + override fun visitObject(properties: List<ProtocolMetaModel.ObjectProperty>?) = TypeDescriptor(scope.generateNestedObject(itemDescriptor.description, properties), itemDescriptor) + + override fun visitUnknown() = TypeDescriptor(BoxableType.STRING, itemDescriptor, true) + }) + } + + private fun generateParserInterfaceList(out: TextOutput) { + // write classes in stable order + Collections.sort(jsonProtocolParserClassNames) + + out.newLine().newLine().append("val PARSER_CLASSES = arrayOf(").newLine() + for (name in jsonProtocolParserClassNames) { + out.append(" ").append(name).append("::class.java") + if (name != jsonProtocolParserClassNames.last()) { + out.append(',') + } + out.newLine() + } + out.append(')') + } + + private fun generateParserRoot(parserRootInterfaceItems: List<ParserRootInterfaceItem>, out: TextOutput) { + // write classes in stable order + Collections.sort<ParserRootInterfaceItem>(parserRootInterfaceItems) + + out.newLine().newLine().append("interface ").append(READER_INTERFACE_NAME).append(" : org.jetbrains.jsonProtocol.ResponseResultReader").openBlock() + for (item in parserRootInterfaceItems) { + item.writeCode(out) + out.newLine() + } + out.append("override fun readResult(methodName: String, reader: org.jetbrains.io.JsonReaderEx): Any? = ") + out.append("when (methodName)").block { + for (item in parserRootInterfaceItems) { + out.append('"') + if (!item.domain.isEmpty()) { + out.append(item.domain).append('.') + } + out.append(item.name).append('"').append(" -> ") + item.appendReadMethodName(out) + out.append("(reader)").newLine() + } + out.append("else -> null") + } + + out.closeBlock() + } + + /** + * Resolve absolute (DOMAIN.TYPE) or relative (TYPE) type name + */ + private fun resolveRefType(scopeDomainName: String, refName: String, direction: TypeData.Direction): BoxableType { + val pos = refName.indexOf('.') + val domainName: String + val shortName: String + if (pos == -1) { + domainName = scopeDomainName + shortName = refName + } + else { + domainName = refName.substring(0, pos) + shortName = refName.substring(pos + 1) + } + return typeMap.resolve(domainName, shortName, direction)!! + } +} + +val READER_INTERFACE_NAME: String = "ProtocolResponseReader" + +private val INCLUDED_DOMAINS = arrayOf("CSS", "Debugger", "DOM", "Inspector", "Log", "Network", "Page", "Runtime", "ServiceWorker", + "Tracing", "Target", "Overlay", "Console", "DOMDebugger", "Profiler", "HeapProfiler", "NodeWorker") + +fun generateMethodNameSubstitute(originalName: String, out: TextOutput): String { + if (originalName != "this") { + return originalName + } + out.append("@org.jetbrains.jsonProtocol.ProtocolName(\"").append(originalName).append("\")").newLine() + return "get${Character.toUpperCase(originalName.get(0))}${originalName.substring(1)}" +} + +fun capitalizeFirstChar(s: String): String { + if (!s.isEmpty() && s.get(0).isLowerCase()) { + return s.get(0).toUpperCase() + s.substring(1) + } + return s +} + +fun <R> switchByType(typedObject: ItemDescriptor, visitor: TypeVisitor<R>): R { + val refName = if (typedObject is ItemDescriptor.Referenceable) typedObject.ref else null + if (refName != null) { + return visitor.visitRef(refName) + } + val typeName = typedObject.type + return when (typeName) { + BOOLEAN_TYPE -> visitor.visitBoolean() + STRING_TYPE -> if (typedObject.enum == null) visitor.visitString() else visitor.visitEnum(typedObject.enum!!) + INTEGER_TYPE, "int" -> visitor.visitInteger() + NUMBER_TYPE -> visitor.visitNumber() + ARRAY_TYPE -> visitor.visitArray(typedObject.items!!) + OBJECT_TYPE -> { + if (typedObject !is ItemDescriptor.Type) { + visitor.visitObject(null) + } + else { + val properties = typedObject.properties + return if (properties.isNullOrEmpty()) visitor.visitMap() else visitor.visitObject(properties) + } + } + ANY_TYPE, UNKNOWN_TYPE -> return visitor.visitUnknown() + else -> throw RuntimeException("Unrecognized type $typeName") + } +}
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-model-generator/src/InputClassScope.kt b/platform/script-debugger/protocol/protocol-model-generator/src/InputClassScope.kt new file mode 100644 index 00000000..a8bf4470 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-model-generator/src/InputClassScope.kt @@ -0,0 +1,73 @@ +package org.jetbrains.protocolModelGenerator + +import org.jetbrains.jsonProtocol.ItemDescriptor +import org.jetbrains.jsonProtocol.ProtocolMetaModel +import org.jetbrains.protocolReader.TextOutput + +internal class InputClassScope(generator: DomainGenerator, namePath: NamePath) : ClassScope(generator, namePath) { + override val typeDirection = TypeData.Direction.INPUT + + fun generateDeclarationBody(out: TextOutput, list: List<ItemDescriptor.Named>) { + for (i in 0 until list.size) { + val named = list[i] + if (named.description != null) { + out.doc(named.description) + } + + val name = named.getName() + val declarationName = generateMethodNameSubstitute(name, out) + + if (classContextNamespace.lastComponent == "RemoteObjectValue" && name == "value") { + // specification says it is string, but it can be map or any other JSON too, so set Any? type + out.append("@org.jetbrains.jsonProtocol.JsonField(allowAnyPrimitiveValue=true)\n" + + " @Optional\n" + + " fun value(): Any?\n\n ") + continue + } + + val typeDescriptor = InputMemberScope(name).resolveType(named) + writeMember(out, typeDescriptor, declarationName) + if (i != (list.size - 1)) { + out.newLine().newLine() + } + } + } + + inner class InputMemberScope(memberName: String) : MemberScope(this@InputClassScope, memberName) { + override fun generateNestedObject(description: String?, properties: List<ProtocolMetaModel.ObjectProperty>?): BoxableType { + val objectName = capitalizeFirstChar(memberName) + addMember { out -> + out.newLine().newLine().doc(description) + if (properties == null) { + out.append("interface ").append(objectName).append(" : JsonObjectBased").openBlock() + } + else { + out.append("@JsonType").newLine() + out.append("interface ").append(objectName).openBlock() + for (property in properties) { + out.doc(property.description) + + val methodName = generateMethodNameSubstitute(property.getName(), out) + val memberScope = InputMemberScope(property.getName()) + val typeDescriptor = memberScope.resolveType(property) + writeMember(out, typeDescriptor, methodName) + } + } + out.closeBlock() + } + return subMessageType(NamePath(objectName, classContextNamespace)) + } + } + + private fun writeMember(out: TextOutput, typeDescriptor: TypeDescriptor, name: String) { + typeDescriptor.writeAnnotations(out) + val asProperty = typeDescriptor.isPrimitive || typeDescriptor.isNullableType + out.append(if (asProperty) "val" else "fun") + out.append(" ").appendEscapedName(name) + out.append(if (asProperty) ": " else "(): ") + out.append(typeDescriptor.type.getShortText(classContextNamespace)) + if (typeDescriptor.isNullableType) { + out.append('?') + } + } +}
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-model-generator/src/ListType.kt b/platform/script-debugger/protocol/protocol-model-generator/src/ListType.kt new file mode 100644 index 00000000..24c3d3d6 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-model-generator/src/ListType.kt @@ -0,0 +1,31 @@ +package org.jetbrains.protocolModelGenerator + +open class ListType(private val itemType: BoxableType) : BoxableType { + override val defaultValue: Nothing? = null + + override val writeMethodName: String + get() = when { + itemType == BoxableType.STRING -> "writeStringList" + itemType == BoxableType.LONG -> "writeLongArray" + itemType == BoxableType.INT -> "writeIntArray" + itemType == BoxableType.NUMBER -> "writeDoubleArray" + itemType == BoxableType.NUMBER -> "writeDoubleArray" + itemType is StandaloneType && itemType.writeMethodName == "writeEnum" -> "writeEnumList" + else -> "writeList" + } + + override val fullText: CharSequence + get() { + if (itemType == BoxableType.LONG || itemType == BoxableType.INT || itemType == BoxableType.NUMBER) { + return "Array<${itemType.fullText}>" + } + return "List<${itemType.fullText}>" + } + + override fun getShortText(contextNamespace: NamePath): String { + if (itemType == BoxableType.LONG || itemType == BoxableType.INT || itemType == BoxableType.NUMBER) { + return "${itemType.fullText}Array" + } + return "List<${itemType.getShortText(contextNamespace)}>" + } +}
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-model-generator/src/MemberScope.kt b/platform/script-debugger/protocol/protocol-model-generator/src/MemberScope.kt new file mode 100644 index 00000000..44c8de3e --- /dev/null +++ b/platform/script-debugger/protocol/protocol-model-generator/src/MemberScope.kt @@ -0,0 +1,37 @@ +package org.jetbrains.protocolModelGenerator + +import com.intellij.openapi.util.text.StringUtil +import org.jetbrains.jsonProtocol.ItemDescriptor +import org.jetbrains.protocolReader.appendEnums + +/** + * Member scope is used to generate additional types that are used only from method. + * These types will be named after this method. + */ +internal open class MemberScope(private val classScope: ClassScope, protected val memberName: String) : ResolveAndGenerateScope { + override fun <T : ItemDescriptor> resolveType(typedObject: T) = classScope.generator.generator.resolveType(typedObject, this) + + fun generateEnum(description: String?, enumConstants: List<String>): BoxableType { + var enumName = capitalizeFirstChar(memberName) + if (StringUtil.equalsIgnoreCase(enumName, "TYPE") && + classScope.classContextNamespace.lastComponent.endsWith("EventData") ) { + // to avoid same name with companion object TYPE + enumName = "EventType" + } + val namePath = NamePath(enumName, classScope.classContextNamespace) + var type = classScope.generator.generator.nestedTypeMap.get(namePath) + if (type == null) { + type = StandaloneType(namePath, "writeEnum") + classScope.generator.generator.nestedTypeMap.put(namePath, type) + classScope.addMember { out -> + out.newLine().doc(description) + appendEnums(enumConstants, enumName, classScope.typeDirection == TypeData.Direction.INPUT, out) + } + } + return type + } + + override fun getDomainName() = classScope.generator.domain.domain() + + override fun getTypeDirection() = classScope.typeDirection +} diff --git a/platform/script-debugger/protocol/protocol-model-generator/src/MyCreateStandaloneTypeBindingVisitorBase.kt b/platform/script-debugger/protocol/protocol-model-generator/src/MyCreateStandaloneTypeBindingVisitorBase.kt new file mode 100644 index 00000000..1dcbe8f0 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-model-generator/src/MyCreateStandaloneTypeBindingVisitorBase.kt @@ -0,0 +1,40 @@ +package org.jetbrains.protocolModelGenerator + +import org.jetbrains.jsonProtocol.ProtocolMetaModel +import org.jetbrains.protocolReader.appendEnums + +internal class MyCreateStandaloneTypeBindingVisitorBase(private val generator: DomainGenerator, type: ProtocolMetaModel.StandaloneType, private val name: String) : CreateStandaloneTypeBindingVisitorBase(generator, type) { + override fun visitObject(properties: List<ProtocolMetaModel.ObjectProperty>?): StandaloneTypeBinding { + return object : StandaloneTypeBinding { + override fun getJavaType() = subMessageType(generator.generator.naming.additionalParam.getFullName(generator.domain.domain(), name)) + + override fun generate() = generator.generateCommandAdditionalParam(type) + + override fun getDirection() = TypeData.Direction.OUTPUT + } + } + + override fun visitEnum(enumConstants: List<String>): StandaloneTypeBinding { + return object : StandaloneTypeBinding { + override fun getJavaType(): BoxableType = StandaloneType(generator.generator.naming.additionalParam.getFullName(generator.domain.domain(), name), "writeEnum") + + override fun generate() = appendEnums(enumConstants, name, false, generator.fileUpdater.out.newLine().newLine()) + + override fun getDirection() = TypeData.Direction.OUTPUT + } + } + + override fun visitArray(items: ProtocolMetaModel.ArrayItemType) = generator.createTypedefTypeBinding(type, object : Target { + override fun resolve(context: Target.ResolveContext): BoxableType { + return ListType(generator.generator.resolveType(items, object : ResolveAndGenerateScope { + // This class is responsible for generating ad hoc type. + // If we ever are to do it, we should generate into string buffer and put strings inside TypeDef class + override fun getDomainName() = generator.domain.domain() + + override fun getTypeDirection() = TypeData.Direction.OUTPUT + + override fun generateNestedObject(description: String?, properties: List<ProtocolMetaModel.ObjectProperty>?) = context.generateNestedObject("Item", description, properties) + }).type) + } + }, generator.generator.naming.outputTypedef, TypeData.Direction.OUTPUT) +} diff --git a/platform/script-debugger/protocol/protocol-model-generator/src/NamePath.kt b/platform/script-debugger/protocol/protocol-model-generator/src/NamePath.kt new file mode 100644 index 00000000..aab52a4f --- /dev/null +++ b/platform/script-debugger/protocol/protocol-model-generator/src/NamePath.kt @@ -0,0 +1,29 @@ +package org.jetbrains.protocolModelGenerator + +data class NamePath(val lastComponent: String, val parent: NamePath? = null) { + fun getLength(): Int { + var res = 1 + run { + var current: NamePath? = this + while (current != null) { + res++ + current = current.parent + } + } + return res + } + + fun getFullText(): String { + val result = StringBuilder() + fillFullPath(result) + return result.toString() + } + + private fun fillFullPath(result: StringBuilder) { + if (parent != null) { + parent.fillFullPath(result) + result.append('.') + } + result.append(lastComponent) + } +} diff --git a/platform/script-debugger/protocol/protocol-model-generator/src/OutputClassScope.kt b/platform/script-debugger/protocol/protocol-model-generator/src/OutputClassScope.kt new file mode 100644 index 00000000..55d6f089 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-model-generator/src/OutputClassScope.kt @@ -0,0 +1,61 @@ +package org.jetbrains.protocolModelGenerator + +import org.jetbrains.jsonProtocol.ItemDescriptor +import org.jetbrains.jsonProtocol.ProtocolMetaModel +import org.jetbrains.protocolReader.TextOutput + +internal class OutputClassScope(generator: DomainGenerator, classNamePath: NamePath) : ClassScope(generator, classNamePath) { + fun <P : ItemDescriptor.Named> writeWriteCalls(out: TextOutput, parameters: List<Pair<P, BoxableType>>, qualifier: String?) { + for ((descriptor, type) in parameters) { + out.newLine() + if (qualifier != null) { + out.append(qualifier).append('.') + } + appendWriteValueInvocation(out, descriptor, descriptor.name(), type) + } + } + + fun <P : ItemDescriptor.Named> writeMethodParameters(out: TextOutput, parameters: List<Pair<P, BoxableType>>, prependComma: Boolean) { + var needComa = prependComma + for ((descriptor, type) in parameters) { + if (needComa) { + out.comma() + } + else { + needComa = true + } + + val shortText = type.getShortText(classContextNamespace) + out.append(descriptor.name()).append(": ") + out.append(if (shortText == "String") "CharSequence" else shortText) + if (descriptor.optional) { + val defaultValue: String + if (descriptor is ProtocolMetaModel.Parameter && descriptor.default != null) { + defaultValue = descriptor.default!! + } + else { + defaultValue = type.defaultValue ?: "null" + if (defaultValue == "null") { + out.append('?') + } + } + out.append(" = ").append(defaultValue) + } + } + } + + private fun appendWriteValueInvocation(out: TextOutput, descriptor: ItemDescriptor.Named, valueRefName: String, type: BoxableType) { + // todo CallArgument (we should allow write null as value) + val allowNullableString = descriptor.name() == "value" && type.writeMethodName == "writeString" + out.append(if (allowNullableString) "writeNullableString" else type.writeMethodName).append('(') + out.quote(descriptor.name()).comma().append(valueRefName) + + if (!allowNullableString && descriptor.optional && type.defaultValue != null && type != BoxableType.MAP) { + out.comma().append(type.defaultValue!!) + } + + out.append(')') + } + + override val typeDirection = TypeData.Direction.OUTPUT +} diff --git a/platform/script-debugger/protocol/protocol-model-generator/src/ParserRootInterfaceItem.kt b/platform/script-debugger/protocol/protocol-model-generator/src/ParserRootInterfaceItem.kt new file mode 100644 index 00000000..3854bb0e --- /dev/null +++ b/platform/script-debugger/protocol/protocol-model-generator/src/ParserRootInterfaceItem.kt @@ -0,0 +1,27 @@ +package org.jetbrains.protocolModelGenerator + +import org.jetbrains.protocolReader.JSON_READER_PARAMETER_DEF +import org.jetbrains.protocolReader.TextOutput + +class ParserRootInterfaceItem(val domain: String, val name: String, private val nameScheme: ClassNameScheme.Input) : Comparable<ParserRootInterfaceItem> { + val fullName: String + + init { + fullName = nameScheme.getFullName(domain, name).getFullText() + } + + fun writeCode(out: TextOutput) { + out.append("@JsonParseMethod").newLine() + out.append("fun ") + appendReadMethodName(out) + out.append('(').append(JSON_READER_PARAMETER_DEF).append("): ").append(fullName).newLine() + } + + fun appendReadMethodName(out: TextOutput) { + out.append(nameScheme.getParseMethodName(domain, name)) + } + + override fun compareTo(other: ParserRootInterfaceItem): Int { + return fullName.compareTo(other.fullName) + } +} diff --git a/platform/script-debugger/protocol/protocol-model-generator/src/ResolveAndGenerateScope.kt b/platform/script-debugger/protocol/protocol-model-generator/src/ResolveAndGenerateScope.kt new file mode 100644 index 00000000..87042eb3 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-model-generator/src/ResolveAndGenerateScope.kt @@ -0,0 +1,13 @@ +package org.jetbrains.protocolModelGenerator + +import org.jetbrains.jsonProtocol.ItemDescriptor +import org.jetbrains.jsonProtocol.ProtocolMetaModel + +internal interface ResolveAndGenerateScope { + fun getDomainName(): String + fun getTypeDirection(): TypeData.Direction + + fun <T : ItemDescriptor> resolveType(typedObject: T): TypeDescriptor = throw UnsupportedOperationException() + + open fun generateNestedObject(description: String?, properties: List<ProtocolMetaModel.ObjectProperty>?): BoxableType = throw UnsupportedOperationException() +} diff --git a/platform/script-debugger/protocol/protocol-model-generator/src/StandaloneType.kt b/platform/script-debugger/protocol/protocol-model-generator/src/StandaloneType.kt new file mode 100644 index 00000000..76964aab --- /dev/null +++ b/platform/script-debugger/protocol/protocol-model-generator/src/StandaloneType.kt @@ -0,0 +1,49 @@ +package org.jetbrains.protocolModelGenerator + +class StandaloneType(private val namePath: NamePath, override val writeMethodName: String, override val defaultValue: String? = "null") : BoxableType { + override val fullText: String = namePath.getFullText() + + override fun getShortText(contextNamespace: NamePath): String { + val nameLength = namePath.getLength() + val contextLength = contextNamespace.getLength() + if (nameLength > contextLength) { + val builder = subtractContextRecursively(namePath, nameLength - contextLength, contextNamespace) + if (builder != null) { + return builder.toString() + } + } + return namePath.getFullText() + } + + private fun subtractContextRecursively(namePos: NamePath?, count: Int, prefix: NamePath): StringBuilder? { + if (count > 1) { + val result = subtractContextRecursively(namePos!!.parent, count - 1, prefix) ?: return null + result.append('.') + result.append(namePos.lastComponent) + return result + } + else { + var namePos = namePos + var prefix = prefix + val nameComponent = namePos!!.lastComponent + namePos = namePos.parent + do { + if (namePos!!.lastComponent != prefix.lastComponent) { + return null + } + + namePos = namePos.parent + if (namePos == null) { + break + } + + prefix = prefix.parent!! + } + while (true) + + val result = StringBuilder() + result.append(nameComponent) + return result + } + } +}
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-model-generator/src/StandaloneTypeBinding.kt b/platform/script-debugger/protocol/protocol-model-generator/src/StandaloneTypeBinding.kt new file mode 100644 index 00000000..1754c339 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-model-generator/src/StandaloneTypeBinding.kt @@ -0,0 +1,33 @@ +package org.jetbrains.protocolModelGenerator + +import org.jetbrains.jsonProtocol.ProtocolMetaModel + +internal interface StandaloneTypeBinding { + fun getJavaType(): BoxableType + + fun generate() + + /** + * @return null if not direction-specific + */ + fun getDirection(): TypeData.Direction? +} + +internal interface Target { + fun resolve(context: ResolveContext): BoxableType + + interface ResolveContext { + fun generateNestedObject(shortName: String, description: String?, properties: List<ProtocolMetaModel.ObjectProperty>?): BoxableType + } +} + +internal class PredefinedTarget(private val resolvedType: BoxableType) : Target { + override fun resolve(context: Target.ResolveContext) = resolvedType + + companion object { + val STRING = PredefinedTarget(BoxableType.STRING) + val INT = PredefinedTarget(BoxableType.INT) + val NUMBER = PredefinedTarget(BoxableType.NUMBER) + val MAP = PredefinedTarget(BoxableType.MAP) + } +}
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-model-generator/src/TypeData.kt b/platform/script-debugger/protocol/protocol-model-generator/src/TypeData.kt new file mode 100644 index 00000000..934309b5 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-model-generator/src/TypeData.kt @@ -0,0 +1,104 @@ +package org.jetbrains.protocolModelGenerator + +import org.jetbrains.jsonProtocol.ProtocolMetaModel + +internal val ANY = object : StandaloneTypeBinding { + override fun getJavaType() = BoxableType.ANY_STRING + + override fun generate() { + } + + override fun getDirection() = null +} + +internal class TypeData(private val name: String) { + val input by lazy { Input() } + + val output by lazy { Output() } + + internal var type: ProtocolMetaModel.StandaloneType? = null + + private var commonBinding: StandaloneTypeBinding? = null + + enum class Direction { + INPUT { + override fun get(typeData: TypeData) = typeData.input + }, + OUTPUT { + override fun get(typeData: TypeData) = typeData.output + }; + + abstract fun get(typeData: TypeData): TypeRef + } + + abstract inner class TypeRef { + private var oneDirectionBinding: StandaloneTypeBinding? = null + + fun resolve(typeMap: TypeMap, domainGenerator: DomainGenerator): BoxableType? { + if (commonBinding != null) { + return commonBinding!!.getJavaType() + } + if (oneDirectionBinding != null) { + return oneDirectionBinding!!.getJavaType() + } + val binding = resolveImpl(domainGenerator) ?: return null + + if (binding.getDirection() == null) { + commonBinding = binding + } + else { + oneDirectionBinding = binding + } + typeMap.addTypeToGenerate(binding) + return binding.getJavaType() + } + + abstract fun resolveImpl(domainGenerator: DomainGenerator): StandaloneTypeBinding? + } + + inner class Output : TypeRef() { + override fun resolveImpl(domainGenerator: DomainGenerator): StandaloneTypeBinding? { + if (type == null) { + if (name == "int") { + return object : StandaloneTypeBinding { + override fun getJavaType() = BoxableType.INT + + override fun generate() { + } + + override fun getDirection() = null + } + } + else if (name == "any") { + return ANY + } + + throw RuntimeException() + } + return domainGenerator.createStandaloneOutputTypeBinding(type!!, name) + } + } + + inner class Input : TypeRef() { + override fun resolveImpl(domainGenerator: DomainGenerator): StandaloneTypeBinding? { + if (type == null) { + if (name == "int") { + return object : StandaloneTypeBinding { + override fun getJavaType() = BoxableType.INT + + override fun generate() { + } + + override fun getDirection() = null + } + } + else if (name == "any") { + return ANY + } + + throw RuntimeException() + } + return domainGenerator.createStandaloneInputTypeBinding(type!!) + } + } +}
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-model-generator/src/TypeDescriptor.kt b/platform/script-debugger/protocol/protocol-model-generator/src/TypeDescriptor.kt new file mode 100644 index 00000000..13d7b28c --- /dev/null +++ b/platform/script-debugger/protocol/protocol-model-generator/src/TypeDescriptor.kt @@ -0,0 +1,30 @@ +package org.jetbrains.protocolModelGenerator + +import org.jetbrains.jsonProtocol.ItemDescriptor +import org.jetbrains.jsonProtocol.ProtocolMetaModel +import org.jetbrains.protocolReader.TextOutput + +internal class TypeDescriptor(val type: BoxableType, val descriptor: ItemDescriptor, private val asRawString: Boolean = false) { + private val optional = descriptor is ItemDescriptor.Named && descriptor.optional + + fun writeAnnotations(out: TextOutput) { + val default = (descriptor as? ProtocolMetaModel.Parameter)?.default + if (default != null) { + out.append("@Optional").append("(\"").append(default).append("\")").newLine() + } + + if (asRawString) { + out.append("@org.jetbrains.jsonProtocol.JsonField(") + if (asRawString) { + out.append("allowAnyPrimitiveValue=true") + } + out.append(")").newLine() + } + } + + val isNullableType: Boolean + get() = !isPrimitive && (optional || asRawString) + + val isPrimitive: Boolean + get() = type == BoxableType.BOOLEAN || type == BoxableType.INT || type == BoxableType.LONG || type == BoxableType.NUMBER +} diff --git a/platform/script-debugger/protocol/protocol-model-generator/src/TypeMap.kt b/platform/script-debugger/protocol/protocol-model-generator/src/TypeMap.kt new file mode 100644 index 00000000..662b545e --- /dev/null +++ b/platform/script-debugger/protocol/protocol-model-generator/src/TypeMap.kt @@ -0,0 +1,50 @@ +package org.jetbrains.protocolModelGenerator + +import gnu.trove.THashMap +import java.util.* + +/** + * Keeps track of all referenced types. + * A type may be used and resolved (generated or hard-coded). + */ +internal class TypeMap { + private val map = THashMap<Pair<String, String>, TypeData>() + + var domainGeneratorMap: Map<String, DomainGenerator>? = null + + private val typesToGenerate = ArrayDeque<StandaloneTypeBinding>() + + fun resolve(domainName: String, typeName: String, direction: TypeData.Direction): BoxableType? { + val domainGenerator = domainGeneratorMap!!.get(domainName) + if (domainGenerator == null) { + val qName = "$domainName.$typeName"; + if (qName == "IO.StreamHandle" || + qName == "Security.SecurityState" || + qName == "Security.CertificateId" || + qName == "Emulation.ScreenOrientation" || + qName == "Security.MixedContentType" + ) { + return BoxableType.ANY_STRING // ignore + } + throw RuntimeException("Failed to find domain generator: $domainName for type $typeName") + } + return direction.get(getTypeData(domainName, typeName)).resolve(this, domainGenerator) + } + + fun addTypeToGenerate(binding: StandaloneTypeBinding) { + typesToGenerate.offer(binding) + } + + fun generateRequestedTypes() { + // size may grow during iteration + val createdTypes = HashSet<CharSequence>() + while (typesToGenerate.isNotEmpty()) { + val binding = typesToGenerate.poll() + if (createdTypes.add(binding.getJavaType().fullText)) { + binding.generate() + } + } + } + + fun getTypeData(domainName: String, typeName: String) = map.getOrPut(Pair(domainName, typeName)) { TypeData(typeName) } +} diff --git a/platform/script-debugger/protocol/protocol-model-generator/src/TypeVisitor.kt b/platform/script-debugger/protocol/protocol-model-generator/src/TypeVisitor.kt new file mode 100644 index 00000000..87531bb2 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-model-generator/src/TypeVisitor.kt @@ -0,0 +1,25 @@ +package org.jetbrains.protocolModelGenerator + +import org.jetbrains.jsonProtocol.ProtocolMetaModel + +interface TypeVisitor<R> { + fun visitRef(refName: String): R + + fun visitBoolean(): R + + fun visitEnum(enumConstants: List<String>): R + + fun visitString(): R + + fun visitInteger(): R + + fun visitNumber(): R + + fun visitArray(items: ProtocolMetaModel.ArrayItemType): R + + fun visitObject(properties: List<ProtocolMetaModel.ObjectProperty>?): R + + fun visitMap(): R + + fun visitUnknown(): R +}
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-reader-runtime/intellij.platform.scriptDebugger.protocolReaderRuntime.iml b/platform/script-debugger/protocol/protocol-reader-runtime/intellij.platform.scriptDebugger.protocolReaderRuntime.iml new file mode 100644 index 00000000..20648d86 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader-runtime/intellij.platform.scriptDebugger.protocolReaderRuntime.iml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + <orderEntry type="library" exported="" name="gson" level="project" /> + <orderEntry type="module" module-name="intellij.platform.util" /> + <orderEntry type="module" module-name="intellij.platform.ide.impl" /> + <orderEntry type="library" name="netty-codec-http" level="project" /> + </component> +</module>
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-reader-runtime/src/annotations.kt b/platform/script-debugger/protocol/protocol-reader-runtime/src/annotations.kt new file mode 100644 index 00000000..33667d71 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader-runtime/src/annotations.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.jsonProtocol + +@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION) +annotation class JsonField( + val allowAnyPrimitiveValue: Boolean = false, // read any primitive value as String (true as true, number as string - don't try to parse) + val allowAnyPrimitiveValueAndMap: Boolean = false, + val primitiveValue: String = "") + +@Target(AnnotationTarget.CLASS) +annotation class JsonType() + +@Target(AnnotationTarget.FUNCTION) +annotation class JsonSubtypeCasting(val reinterpret: Boolean = false) + +@Target(AnnotationTarget.FUNCTION) +annotation class JsonParseMethod + +/** + * For field-reading method specifies that the field is optional and may safely be absent in + * JSON object. By default fields are not optional. + */ +@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION) +annotation class Optional(val default: String = "") + +@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION) +annotation class ProtocolName(val name: String)
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/EventMap.kt b/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/EventMap.kt new file mode 100644 index 00000000..a2fad496 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/EventMap.kt @@ -0,0 +1,50 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.jsonProtocol + +import com.intellij.util.containers.ContainerUtil +import org.jetbrains.io.JsonReaderEx + +abstract class EventType<T, R : ResponseResultReader>(val methodName: String) { + abstract fun read(protocolReader: R, reader: JsonReaderEx): T +} + +class EventMap<R : ResponseResultReader>(private val protocolReader: R) { + private val nameToHandler = ContainerUtil.newConcurrentMap<String, MutableList<(Any?) -> Unit>>() + private val nameToType = ContainerUtil.newConcurrentMap<String, EventType<*, R>>() + + fun <T : Any?> add(type: EventType<T, R>, handler: (T) -> Unit) { + nameToType.put(type.methodName, type) + @Suppress("UNCHECKED_CAST") + nameToHandler.getOrPut(type.methodName, { ContainerUtil.createLockFreeCopyOnWriteList() }).add(handler as (Any?) -> Unit) + } + + fun <T : Any?> remove(type: EventType<T, R>, handler: (T) -> Unit) { + @Suppress("UNCHECKED_CAST") + nameToHandler[type.methodName]?.remove(handler as (Any?) -> Unit) + } + + fun <T> addMulti(vararg types: EventType<out T, R>, eventHandler: (T) -> Unit) { + for (type in types) { + add(type, eventHandler) + } + } + + fun handleEvent(method: String, data: JsonReaderEx?) { + val handlers = nameToHandler.get(method) + if (handlers == null || handlers.isEmpty()) { + return + } + + val eventData = data?.let { nameToType[method]!!.read(protocolReader, it) } + for (handler in handlers) { + handler(eventData) + } + } + + fun <T : Any?> handleEvent(type: EventType<T, R>, event: T) { + val handlers = nameToHandler.get(type.methodName) ?: return + for (handler in handlers) { + handler(event) + } + } +}
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/JsonObjectBased.java b/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/JsonObjectBased.java new file mode 100644 index 00000000..99fe39c8 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/JsonObjectBased.java @@ -0,0 +1,15 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.jetbrains.jsonProtocol; + +import org.jetbrains.io.JsonReaderEx; + +/** + * Optional base interface for JSON type interface. Underlying JSON object becomes available + * to user this way. + */ +public interface JsonObjectBased { + JsonReaderEx getDeferredReader(); +}
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/JsonReaders.java b/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/JsonReaders.java new file mode 100644 index 00000000..a20a2f48 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/JsonReaders.java @@ -0,0 +1,291 @@ +package org.jetbrains.jsonProtocol; + +import com.google.gson.stream.JsonToken; +import com.intellij.util.ArrayUtilRt; +import gnu.trove.TDoubleArrayList; +import gnu.trove.THashMap; +import gnu.trove.TIntArrayList; +import gnu.trove.TLongArrayList; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.io.JsonReaderEx; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public final class JsonReaders { + public static final ObjectFactory<String> STRING_OBJECT_FACTORY = new ObjectFactory<String>() { + @Override + public String read(JsonReaderEx reader) { + return reader.nextString(); + } + }; + + private JsonReaders() { + } + + public static <T> ObjectFactory<Map<String, T>> mapFactory(@NotNull ObjectFactory<? extends T> valueFactory) { + return new MapFactory<>(valueFactory); + } + + private static void checkIsNull(JsonReaderEx reader, String fieldName) { + if (reader.peek() == JsonToken.NULL) { + throw new RuntimeException("Field is not nullable" + (fieldName == null ? "" : (": " + fieldName))); + } + } + + public static String readRawString(JsonReaderEx reader) { + return reader.nextString(true); + } + + public static Object readRawStringOrMap(JsonReaderEx reader) { + if (reader.peek() == JsonToken.BEGIN_OBJECT) { + return readMap(reader, null); + } + else { + return reader.nextString(true); + } + } + + // Don't use Guava CaseFormat.*! ObjectWithURL must be converted to OBJECT_WITH_URL + public static String convertRawEnumName(@NotNull String enumValue) { + int n = enumValue.length(); + StringBuilder builder = new StringBuilder(n + 4); + boolean prevIsLowerCase = false; + for (int i = 0; i < n; i++) { + char c = enumValue.charAt(i); + if (c == '-' || c == ' ') { + builder.append('_'); + continue; + } + + if (Character.isUpperCase(c)) { + // second check handle "CSPViolation" (transform to CSP_VIOLATION) + if (prevIsLowerCase || (i != 0 && (i + 1) < n && Character.isLowerCase(enumValue.charAt(i + 1)))) { + builder.append('_'); + } + builder.append(c); + prevIsLowerCase = false; + } + else { + builder.append(Character.toUpperCase(c)); + prevIsLowerCase = true; + } + } + return builder.toString(); + } + + public static <T extends Enum<T>> T readEnum(@NotNull JsonReaderEx reader, @NotNull Class<T> enumClass) { + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + return null; + } + + try { + return Enum.valueOf(enumClass, convertRawEnumName(reader.nextString())); + } + catch (IllegalArgumentException ignored) { + return Enum.valueOf(enumClass, "NO_ENUM_CONST"); + } + } + + public static <T> List<T> readObjectArray(@NotNull JsonReaderEx reader, @NotNull ObjectFactory<? extends T> factory) { + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + return null; + } + + reader.beginArray(); + if (!reader.hasNext()) { + reader.endArray(); + return Collections.emptyList(); + } + + List<T> result = new ArrayList<>(); + do { + result.add(factory.read(reader)); + } + while (reader.hasNext()); + reader.endArray(); + return result; + } + + public static <T> Map<String, T> readMap(@NotNull JsonReaderEx reader, @Nullable ObjectFactory<? extends T> factory) { + if (reader.peek() == JsonToken.NULL) { + reader.skipValue(); + return null; + } + + reader.beginObject(); + if (!reader.hasNext()) { + reader.endObject(); + return Collections.emptyMap(); + } + + Map<String, T> map = new THashMap<>(); + while (reader.hasNext()) { + if (factory == null) { + //noinspection unchecked + map.put(reader.nextName(), (T)read(reader)); + } + else { + map.put(reader.nextName(), factory.read(reader)); + } + } + reader.endObject(); + return map; + } + + public static Object read(JsonReaderEx reader) { + switch (reader.peek()) { + case BEGIN_ARRAY: + return nextList(reader); + + case BEGIN_OBJECT: + reader.beginObject(); + return nextObject(reader); + + case STRING: + return reader.nextString(); + + case NUMBER: + return reader.nextDouble(); + + case BOOLEAN: + return reader.nextBoolean(); + + case NULL: + reader.nextNull(); + return null; + + default: throw new IllegalStateException(); + } + } + + public static Map<String, Object> nextObject(JsonReaderEx reader) { + Map<String, Object> map = new THashMap<>(); + while (reader.hasNext()) { + map.put(reader.nextName(), read(reader)); + } + reader.endObject(); + return map; + } + + public static <T> List<T> nextList(JsonReaderEx reader) { + reader.beginArray(); + if (!reader.hasNext()) { + reader.endArray(); + return Collections.emptyList(); + } + + List<T> list = new ArrayList<>(); + do { + //noinspection unchecked + list.add((T)read(reader)); + } + while (reader.hasNext()); + reader.endArray(); + return list; + } + + public static List<String> readRawStringArray(JsonReaderEx reader) { + reader.beginArray(); + if (!reader.hasNext()) { + reader.endArray(); + return Collections.emptyList(); + } + + List<String> list = new ArrayList<>(); + do { + list.add(reader.nextString(true)); + } + while (reader.hasNext()); + reader.endArray(); + return list; + } + + public static long[] readLongArray(JsonReaderEx reader) { + checkIsNull(reader, null); + reader.beginArray(); + if (!reader.hasNext()) { + reader.endArray(); + return ArrayUtilRt.EMPTY_LONG_ARRAY; + } + + TLongArrayList result = new TLongArrayList(); + do { + result.add(reader.nextLong()); + } + while (reader.hasNext()); + reader.endArray(); + return result.toNativeArray(); + } + + public static double[] readDoubleArray(JsonReaderEx reader) { + checkIsNull(reader, null); + reader.beginArray(); + if (!reader.hasNext()) { + reader.endArray(); + return new double[]{0}; + } + + TDoubleArrayList result = new TDoubleArrayList(); + do { + result.add(reader.nextDouble()); + } + while (reader.hasNext()); + reader.endArray(); + return result.toNativeArray(); + } + + public static int[] readIntArray(JsonReaderEx reader) { + checkIsNull(reader, null); + reader.beginArray(); + if (!reader.hasNext()) { + reader.endArray(); + return ArrayUtilRt.EMPTY_INT_ARRAY; + } + + TIntArrayList result = new TIntArrayList(); + do { + result.add(reader.nextInt()); + } + while (reader.hasNext()); + reader.endArray(); + return result.toNativeArray(); + } + + public static List<StringIntPair> readIntStringPairs(JsonReaderEx reader) { + checkIsNull(reader, null); + reader.beginArray(); + if (!reader.hasNext()) { + reader.endArray(); + return Collections.emptyList(); + } + + List<StringIntPair> result = new ArrayList<>(); + do { + reader.beginArray(); + result.add(new StringIntPair(reader.nextInt(), reader.nextString())); + reader.endArray(); + } + while (reader.hasNext()); + reader.endArray(); + return result; + } + + public static boolean findBooleanField(String name, JsonReaderEx reader) { + reader.beginObject(); + while (reader.hasNext()) { + if (reader.nextName().equals(name)) { + return reader.nextBoolean(); + } + else { + reader.skipValue(); + } + } + return false; + } +}
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/JsonSubtype.java b/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/JsonSubtype.java new file mode 100644 index 00000000..23879bbf --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/JsonSubtype.java @@ -0,0 +1,14 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.jetbrains.jsonProtocol; + +/** + * A base interface for JSON subtype interface. This inheritance serves 2 purposes: + * it declares base type (visible to human and to interface analyzer) and adds {@link #getBase()} + * getter that may be directly used in programs. + */ +public interface JsonSubtype<BASE> { + BASE getBase(); +} diff --git a/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/JsonWriters.kt b/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/JsonWriters.kt new file mode 100644 index 00000000..cbdd2a4c --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/JsonWriters.kt @@ -0,0 +1,21 @@ +package org.jetbrains.jsonProtocol + +import com.google.gson.stream.JsonWriter +import java.lang.reflect.Method + +object JsonWriters { + val JSON_WRITE_DEFERRED_NAME: Method + + init { + JSON_WRITE_DEFERRED_NAME = JsonWriter::class.java.getDeclaredMethod("writeDeferredName") + JSON_WRITE_DEFERRED_NAME.isAccessible = true + } + + fun writeStringList(writer: JsonWriter, name: String, value: Collection<String>) { + writer.name(name).beginArray() + for (item in value) { + writer.value(item) + } + writer.endArray() + } +}
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/MapFactory.java b/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/MapFactory.java new file mode 100644 index 00000000..d602df9e --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/MapFactory.java @@ -0,0 +1,19 @@ +package org.jetbrains.jsonProtocol; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.io.JsonReaderEx; + +import java.util.Map; + +final class MapFactory<T> extends ObjectFactory<Map<String, T>> { + private final ObjectFactory<? extends T> valueFactory; + + MapFactory(@NotNull ObjectFactory<? extends T> valueFactory) { + this.valueFactory = valueFactory; + } + + @Override + public Map<String, T> read(JsonReaderEx reader) { + return JsonReaders.readMap(reader, valueFactory); + } +} diff --git a/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/ObjectFactory.java b/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/ObjectFactory.java new file mode 100644 index 00000000..2676fd4e --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/ObjectFactory.java @@ -0,0 +1,7 @@ +package org.jetbrains.jsonProtocol; + +import org.jetbrains.io.JsonReaderEx; + +public abstract class ObjectFactory<T> { + public abstract T read(JsonReaderEx reader); +} diff --git a/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/OutMessage.kt b/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/OutMessage.kt new file mode 100644 index 00000000..b8fea3b0 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/OutMessage.kt @@ -0,0 +1,276 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.jsonProtocol + +import com.google.gson.stream.JsonWriter +import com.intellij.openapi.vfs.CharsetToolkit +import com.intellij.util.containers.isNullOrEmpty +import com.intellij.util.io.writeUtf8 +import gnu.trove.TIntArrayList +import gnu.trove.TIntHashSet +import io.netty.buffer.ByteBuf +import io.netty.buffer.ByteBufAllocator +import io.netty.buffer.ByteBufUtf8Writer +import org.jetbrains.io.JsonUtil + +open class OutMessage() { + val buffer: ByteBuf = ByteBufAllocator.DEFAULT.heapBuffer() + val writer: JsonWriter = JsonWriter(ByteBufUtf8Writer(buffer)) + + private var finalized: Boolean = false + + init { + writer.beginObject() + } + + open fun beginArguments() { + } + + fun writeMap(name: String, value: Map<String, String>? = null) { + if (value == null) return + + beginArguments() + writer.name(name) + writer.beginObject() + for ((key, value1) in value) { + writer.name(key).value(value1) + } + writer.endObject() + } + + protected fun writeLongArray(name: String, value: LongArray) { + beginArguments() + writer.name(name) + writer.beginArray() + for (v in value) { + writer.value(v) + } + writer.endArray() + } + + fun writeDoubleArray(name: String, value: DoubleArray) { + beginArguments() + writer.name(name) + writer.beginArray() + for (v in value) { + writer.value(v) + } + writer.endArray() + } + + fun writeIntArray(name: String, value: IntArray? = null) { + if (value == null) { + return + } + + beginArguments() + writer.name(name) + writer.beginArray() + for (v in value) { + writer.value(v.toLong()) + } + writer.endArray() + } + + fun writeIntSet(name: String, value: TIntHashSet) { + beginArguments() + writer.name(name) + writer.beginArray() + value.forEach { value -> + writer.value(value.toLong()) + true + } + writer.endArray() + } + + fun writeIntList(name: String, value: TIntArrayList) { + beginArguments() + writer.name(name) + writer.beginArray() + for (i in 0..value.size() - 1) { + writer.value(value.getQuick(i).toLong()) + } + writer.endArray() + } + + fun writeSingletonIntArray(name: String, value: Int) { + beginArguments() + writer.name(name) + writer.beginArray() + writer.value(value.toLong()) + writer.endArray() + } + + fun <E : OutMessage> writeList(name: String, value: List<E>?) { + if (value.isNullOrEmpty()) { + return + } + + beginArguments() + writer.name(name) + writer.beginArray() + var isNotFirst = false + for (item in value!!) { + if (isNotFirst) { + buffer.writeByte(','.toInt()).writeByte(' '.toInt()) + } + else { + isNotFirst = true + } + + if (!item.finalized) { + item.finalized = true + try { + item.writer.endObject() + } + catch (e: IllegalStateException) { + if ("Nesting problem." == e.message) { + throw RuntimeException(item.buffer.toString(CharsetToolkit.UTF8_CHARSET) + "\nparent:\n" + buffer.toString(CharsetToolkit.UTF8_CHARSET), e) + } + else { + throw e + } + } + + } + + buffer.writeBytes(item.buffer) + } + writer.endArray() + } + + fun writeStringList(name: String, value: Collection<String>?) { + if (value == null) return + beginArguments() + JsonWriters.writeStringList(writer, name, value) + } + + fun writeEnumList(name: String, values: Collection<Enum<*>>) { + beginArguments() + writer.name(name).beginArray() + for (item in values) { + writer.value(item.toString()) + } + writer.endArray() + } + + fun writeMessage(name: String, value: OutMessage?) { + if (value == null) { + return + } + + beginArguments() + prepareWriteRaw(this, name) + + if (!value.finalized) { + value.close() + } + buffer.writeBytes(value.buffer) + } + + fun close() { + assert(!finalized) + finalized = true + writer.endObject() + writer.close() + } + + protected fun writeLong(name: String, value: Long) { + beginArguments() + writer.name(name).value(value) + } + + fun writeString(name: String, value: String?) { + if (value != null) { + writeNullableString(name, value) + } + } + + fun writeNullableString(name: String, value: CharSequence?) { + beginArguments() + writer.name(name).value(value?.toString()) + } +} + +fun prepareWriteRaw(message: OutMessage, name: String) { + message.writer.name(name).nullValue() + val itemBuffer = message.buffer + itemBuffer.writerIndex(itemBuffer.writerIndex() - "null".length) +} + +fun doWriteRaw(message: OutMessage, rawValue: String) { + message.buffer.writeUtf8(rawValue) +} + +fun OutMessage.writeEnum(name: String, value: Enum<*>?, defaultValue: Enum<*>?) { + if (value != null && value != defaultValue) { + writeEnum(name, value) + } +} + +fun OutMessage.writeEnum(name: String, value: Enum<*>) { + beginArguments() + writer.name(name).value(value.toString()) +} + +fun OutMessage.writeString(name: String, value: CharSequence?, defaultValue: CharSequence?) { + if (value != null && value != defaultValue) { + writeString(name, value) + } +} + +fun OutMessage.writeString(name: String, value: CharSequence) { + beginArguments() + prepareWriteRaw(this, name) + JsonUtil.escape(value, buffer) +} + +fun OutMessage.writeInt(name: String, value: Int, defaultValue: Int) { + if (value != defaultValue) { + writeInt(name, value) + } +} + +fun OutMessage.writeInt(name: String, value: Int?) { + if (value != null) { + beginArguments() + writer.name(name).value(value.toLong()) + } +} + +fun OutMessage.writeBoolean(name: String, value: Boolean, defaultValue: Boolean) { + if (value != defaultValue) { + writeBoolean(name, value) + } +} + +fun OutMessage.writeBoolean(name: String, value: Boolean?) { + if (value != null) { + beginArguments() + writer.name(name).value(value) + } +} + +fun OutMessage.writeDouble(name: String, value: Double?, defaultValue: Double?) { + if (value != null && value != defaultValue) { + writeDouble(name, value) + } +} + +fun OutMessage.writeDouble(name: String, value: Double) { + beginArguments() + writer.name(name).value(value) +} diff --git a/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/Request.kt b/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/Request.kt new file mode 100644 index 00000000..0276b3ff --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/Request.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.jsonProtocol + +import io.netty.buffer.ByteBuf +import org.jetbrains.io.JsonReaderEx + +interface Request<RESULT> { + val buffer: ByteBuf + + val methodName: String + + fun finalize(id: Int) +} + +interface ResponseResultReader { + fun readResult(methodName: String, reader: JsonReaderEx): Any? +}
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/RequestImpl.kt b/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/RequestImpl.kt new file mode 100644 index 00000000..c5cffef6 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/RequestImpl.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.jsonProtocol + +abstract class RequestImpl<T> : OutMessage(), Request<T> { + private var argumentsObjectStarted = false + + protected abstract val idKeyName: String + + protected abstract fun argumentsKeyName(): String + + override fun beginArguments() { + if (!argumentsObjectStarted) { + argumentsObjectStarted = true + writer.name(argumentsKeyName()) + writer.beginObject() + } + } + + override fun finalize(id: Int) { + if (argumentsObjectStarted) { + writer.endObject() + } + writer.name(idKeyName).value(id.toLong()) + writer.endObject() + writer.close() + } +} diff --git a/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/StringIntPair.java b/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/StringIntPair.java new file mode 100644 index 00000000..cda23f49 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader-runtime/src/org/jetbrains/jsonProtocol/StringIntPair.java @@ -0,0 +1,15 @@ +package org.jetbrains.jsonProtocol; + +public final class StringIntPair { + public final String name; + public final int value; + + public StringIntPair(String name, int value) { + this.name = name; + this.value = value; + } + + public StringIntPair(int value, String name) { + this(name, value); + } +} diff --git a/platform/script-debugger/protocol/protocol-reader/intellij.javascript.protocolReader.iml b/platform/script-debugger/protocol/protocol-reader/intellij.javascript.protocolReader.iml new file mode 100644 index 00000000..4bbaa5d5 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader/intellij.javascript.protocolReader.iml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" packagePrefix="org.jetbrains.protocolReader" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + <orderEntry type="library" name="Trove4j" level="project" /> + <orderEntry type="library" name="gson" level="project" /> + <orderEntry type="module" module-name="intellij.platform.scriptDebugger.protocolReaderRuntime" exported="" /> + <orderEntry type="module" module-name="intellij.platform.ide.impl" /> + <orderEntry type="library" name="KotlinJavaRuntime" level="project" /> + </component> +</module>
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-reader/src/ArrayReader.kt b/platform/script-debugger/protocol/protocol-reader/src/ArrayReader.kt new file mode 100644 index 00000000..29f4861d --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader/src/ArrayReader.kt @@ -0,0 +1,23 @@ +package org.jetbrains.protocolReader + +internal class ArrayReader(private val componentParser: ValueReader, private val isList: Boolean) : ValueReader() { + override fun appendFinishedValueTypeName(out: TextOutput) { + if (isList) { + out.append("List<") + } + else { + if (componentParser is PrimitiveValueReader && (componentParser.className == "Int" || componentParser.className == "Double" || componentParser.className == "Float")) { + out.append(componentParser.className).append("Array") + return + } + + out.append("Array<") + } + componentParser.appendFinishedValueTypeName(out) + out.append('>') + } + + override fun writeReadCode(scope: ClassScope, subtyping: Boolean, out: TextOutput) { + componentParser.writeArrayReadCode(scope, subtyping, out) + } +} diff --git a/platform/script-debugger/protocol/protocol-reader/src/ClassScope.kt b/platform/script-debugger/protocol/protocol-reader/src/ClassScope.kt new file mode 100644 index 00000000..16c73feb --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader/src/ClassScope.kt @@ -0,0 +1,7 @@ +package org.jetbrains.protocolReader + +internal class ClassScope(fileScope: FileScope, private val parentClass: ClassScope?) : FileScope(fileScope.output, fileScope) { + fun getRootClassScope(): ClassScope = if (parentClass == null) this else parentClass.getRootClassScope() + + override fun asClassScope() = this +}
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-reader/src/EnumReader.kt b/platform/script-debugger/protocol/protocol-reader/src/EnumReader.kt new file mode 100644 index 00000000..3f2e3b7b --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader/src/EnumReader.kt @@ -0,0 +1,12 @@ +package org.jetbrains.protocolReader + +internal class EnumReader(private val enumClass: Class<Enum<*>>) : ValueReader() { + override fun appendFinishedValueTypeName(out: TextOutput) { + out.append(enumClass.canonicalName) + } + + override fun writeReadCode(scope: ClassScope, subtyping: Boolean, out: TextOutput) { + beginReadCall("Enum", subtyping, out) + out.comma().append(enumClass.canonicalName).append("::class.java)") + } +} diff --git a/platform/script-debugger/protocol/protocol-reader/src/ExistingSubtypeAspect.kt b/platform/script-debugger/protocol/protocol-reader/src/ExistingSubtypeAspect.kt new file mode 100644 index 00000000..b0fd3039 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader/src/ExistingSubtypeAspect.kt @@ -0,0 +1,33 @@ +package org.jetbrains.protocolReader + +private val BASE_VALUE_PREFIX = "baseMessage" + +internal class ExistingSubtypeAspect(private val jsonSuperClass: TypeRef<*>) { + private var subtypeCaster: SubtypeCaster? = null + + fun setSubtypeCaster(subtypeCaster: SubtypeCaster) { + this.subtypeCaster = subtypeCaster + } + + fun writeGetSuperMethodJava(out: TextOutput) { + out.newLine().append("override fun getBase() = ").append(BASE_VALUE_PREFIX) + } + + fun writeSuperFieldJava(out: TextOutput) { + out.append(", private val ").append(BASE_VALUE_PREFIX).append(": ").append(jsonSuperClass.type!!.typeClass.canonicalName) + } + + fun writeParseMethod(scope: ClassScope, out: TextOutput) { + out.newLine().append("companion object").block { + out.append("fun parse").append('(').append(JSON_READER_PARAMETER_DEF).append(", name: String?").append(") = ") + jsonSuperClass.type!!.writeInstantiateCode(scope, out) + out.append('(').append(READER_NAME).append(", name)").append('.') + subtypeCaster!!.writeJava(out) + } + out.newLine() + } + + fun writeInstantiateCode(className: String, out: TextOutput) { + out.append(className).append(".parse") + } +} diff --git a/platform/script-debugger/protocol/protocol-reader/src/FieldProcessor.kt b/platform/script-debugger/protocol/protocol-reader/src/FieldProcessor.kt new file mode 100644 index 00000000..9bd7ee14 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader/src/FieldProcessor.kt @@ -0,0 +1,197 @@ +package org.jetbrains.protocolReader + +import com.intellij.openapi.util.text.StringUtil +import gnu.trove.THashSet +import org.jetbrains.jsonProtocol.JsonField +import org.jetbrains.jsonProtocol.JsonSubtypeCasting +import org.jetbrains.jsonProtocol.Optional +import org.jetbrains.jsonProtocol.ProtocolName +import java.lang.reflect.Method +import java.lang.reflect.ParameterizedType +import java.util.* +import kotlin.reflect.KCallable +import kotlin.reflect.KFunction +import kotlin.reflect.KProperty +import kotlin.reflect.jvm.javaGetter +import kotlin.reflect.jvm.javaMethod +import kotlin.reflect.jvm.javaType + +internal class FieldLoader(val name: String, val jsonName: String, val valueReader: ValueReader, val skipRead: Boolean, val asImpl: Boolean, val defaultValue: String?) + +internal fun TextOutput.appendName(loader: FieldLoader): TextOutput { + if (!loader.asImpl) { + append(FIELD_PREFIX) + } + append(loader.name) + return this +} + +internal class FieldProcessor(private val reader: InterfaceReader, typeClass: Class<*>) { + val fieldLoaders = ArrayList<FieldLoader>() + val methodHandlerMap = LinkedHashMap<Method, MethodHandler>() + val volatileFields = ArrayList<VolatileFieldBinding>() + var lazyRead: Boolean = false + + init { + val methods = typeClass.methods + // todo sort by source location + Arrays.sort(methods, { o1, o2 -> o1.name.compareTo(o2.name) }) + + val skippedNames = THashSet<String>() + for (method in methods) { + val annotation = method.getAnnotation<JsonField>(JsonField::class.java) + if (annotation != null && !annotation.primitiveValue.isEmpty()) { + skippedNames.add(annotation.primitiveValue) + skippedNames.add("${annotation.primitiveValue}Type") + } + } + + val classPackage = typeClass.`package` + val kClass = typeClass.kotlin + for (member in kClass.members) { + val method = if (member is KProperty<*>) { + member.javaGetter!! + } + else if (member is KFunction<*>) { + member.javaMethod!! + } + else { + continue + } + + val methodClass = method.declaringClass + // use method from super if super located in the same package + if (methodClass != typeClass) { + val methodPackage = methodClass.`package` + if (methodPackage != classPackage && !classPackage.name.startsWith("${methodPackage.name}.")) { + continue + } + } + + if (method.parameterCount != 0) { + throw JsonProtocolModelParseException("No parameters expected in $method") + } + + try { + val methodHandler: MethodHandler + val jsonSubtypeCaseAnnotation = method.getAnnotation(JsonSubtypeCasting::class.java) + if (jsonSubtypeCaseAnnotation == null) { + methodHandler = createMethodHandler(member, method, skippedNames.contains(method.name)) ?: continue + } + else { + methodHandler = processManualSubtypeMethod(member, method, jsonSubtypeCaseAnnotation) + lazyRead = true + } + methodHandlerMap.put(method, methodHandler) + } + catch (e: Exception) { + throw JsonProtocolModelParseException("Problem with method $method", e) + } + } + } + + private fun createMethodHandler(member: KCallable<*>, method: Method, skipRead: Boolean): MethodHandler? { + var protocolName = member.annotation<ProtocolName>()?.name ?: member.name + val genericReturnType = member.returnType.javaType + val isNotNull: Boolean + val isPrimitive = if (genericReturnType is Class<*>) genericReturnType.isPrimitive else genericReturnType !is ParameterizedType + val optionalAnnotation = member.annotation<Optional>() + if (isPrimitive || optionalAnnotation != null) { + isNotNull = false + } + else { + val fieldAnnotation = member.annotation<JsonField>() + if (fieldAnnotation == null) { + isNotNull = !member.returnType.isMarkedNullable + } + else { + isNotNull = !fieldAnnotation.allowAnyPrimitiveValue && !fieldAnnotation.allowAnyPrimitiveValueAndMap + } + } + + val fieldTypeParser = reader.getFieldTypeParser(member, genericReturnType, false, method) + val isProperty = member is KProperty<*> + val isAsImpl = isProperty && !isNotNull + if (fieldTypeParser != VOID_PARSER) { + fieldLoaders.add(FieldLoader(member.name, protocolName, fieldTypeParser, skipRead, isAsImpl, StringUtil.nullize(optionalAnnotation?.default))) + } + + if (isAsImpl) { + return null + } + + val effectiveFieldName = if (fieldTypeParser == VOID_PARSER) null else member.name + return object : MethodHandler { + override fun writeMethodImplementationJava(scope: ClassScope, method: Method, out: TextOutput) { + out.append("override ").append(if (isProperty) "val" else "fun") + out.append(" ").appendEscapedName(method.name) + + if (isProperty) { + out.newLine() + out.indentIn() + out.append("get()") + // todo append type name + } + else { + out.append("()") + } + + if (effectiveFieldName == null) { + out.openBlock() + out.closeBlock() + } + else { + out.append(" = ").append(FIELD_PREFIX).append(effectiveFieldName) + if (isNotNull) { + out.append("!!") + } + + if (isProperty) { + out.indentOut() + } + } + } + } + } + + private fun processManualSubtypeMethod(member: KCallable<*>, m: Method, jsonSubtypeCaseAnn: JsonSubtypeCasting): MethodHandler { + val fieldTypeParser = reader.getFieldTypeParser(member, m.genericReturnType, !jsonSubtypeCaseAnn.reinterpret, null) + val fieldInfo = allocateVolatileField(fieldTypeParser, true) + val handler = LazyCachedMethodHandler(fieldTypeParser, fieldInfo) + val parserAsObjectValueParser = fieldTypeParser.asJsonTypeParser() + if (parserAsObjectValueParser != null && parserAsObjectValueParser.isSubtyping()) { + reader.subtypeCasters.add(object : SubtypeCaster(parserAsObjectValueParser.type) { + override fun writeJava(out: TextOutput) { + out.append(m.name).append("()") + } + }) + } + return handler + } + + private fun allocateVolatileField(fieldTypeParser: ValueReader, internalType: Boolean): VolatileFieldBinding { + val position = volatileFields.size + val fieldTypeInfo: (scope: FileScope, out: TextOutput)->Unit + if (internalType) { + fieldTypeInfo = {scope, out -> fieldTypeParser.appendInternalValueTypeName(scope, out)} + } + else { + fieldTypeInfo = {scope, out -> fieldTypeParser.appendFinishedValueTypeName(out)} + } + val binding = VolatileFieldBinding(position, fieldTypeInfo) + volatileFields.add(binding) + return binding + } +} + +internal inline fun <reified T : Annotation> KCallable<*>.annotation(): T? = annotations.firstOrNull() { it is T } as? T ?: (this as? KFunction<*>)?.javaMethod?.getAnnotation<T>(T::class.java) + +/** + * An internal facility for navigating from object of base type to object of subtype. Used only + * when user wants to parse JSON object as subtype. + */ +internal abstract class SubtypeCaster(private val subtypeRef: TypeRef<*>) { + abstract fun writeJava(out: TextOutput) + + fun getSubtypeHandler() = subtypeRef.type!! +}
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-reader/src/FileScope.kt b/platform/script-debugger/protocol/protocol-reader/src/FileScope.kt new file mode 100644 index 00000000..eb16195e --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader/src/FileScope.kt @@ -0,0 +1,9 @@ +package org.jetbrains.protocolReader + +internal fun FileScope(globalScope: GlobalScope, stringBuilder: StringBuilder) = FileScope(TextOutput(stringBuilder), globalScope) + +internal open class FileScope(val output: TextOutput, globalScope: GlobalScope) : GlobalScope(globalScope.state) { + fun newClassScope() = ClassScope(this, asClassScope()) + + protected open fun asClassScope(): ClassScope? = null +}
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-reader/src/FileUpdater.kt b/platform/script-debugger/protocol/protocol-reader/src/FileUpdater.kt new file mode 100644 index 00000000..d088a16a --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader/src/FileUpdater.kt @@ -0,0 +1,34 @@ +package org.jetbrains.protocolReader + +import java.nio.file.Files +import java.nio.file.Path +import java.util.* + +/** + * A class that makes accurate java source file update. If only header + * (with source file revision and other comments) changed, the file is left intact. + * <p>User first writes all the content into a {@link Writer} provided and then + * calls {@link #update()}. + */ +class FileUpdater(private val file: Path) { + val builder: StringBuilder = StringBuilder() + val out: TextOutput = TextOutput(builder) + + fun update() { + if (builder.length == 0) { + Files.delete(file) + return + } + + val newContent = builder.toString().toByteArray() + if (Files.exists(file)) { + if (Arrays.equals(Files.readAllBytes(file), newContent)) { + return + } + } + else { + Files.createDirectories(file.parent) + } + Files.write(file, newContent) + } +} diff --git a/platform/script-debugger/protocol/protocol-reader/src/GlobalScope.kt b/platform/script-debugger/protocol/protocol-reader/src/GlobalScope.kt new file mode 100644 index 00000000..13ffceb3 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader/src/GlobalScope.kt @@ -0,0 +1,63 @@ +package org.jetbrains.protocolReader + +import gnu.trove.THashMap +import gnu.trove.THashSet +import java.util.* + +internal fun GlobalScope(typeWriters: Collection<TypeWriter<*>?>, basePackages: Collection<Map<Class<*>, String>>) = GlobalScope(State(typeWriters, basePackages)) + +internal open class GlobalScope(val state: State) { + fun getTypeImplReference(typeWriter: TypeWriter<*>) = state.getTypeImplReference(typeWriter) + + fun requireFactoryGenerationAndGetName(typeWriter: TypeWriter<*>) = state.requireFactoryGenerationAndGetName(typeWriter) + + fun getTypeImplShortName(typeWriter: TypeWriter<*>) = state.getTypeImplShortName(typeWriter) + + fun newFileScope(output: StringBuilder) = FileScope(this, output) + + fun getTypeFactories() = state.typesWithFactoriesList +} + +internal class State(typeWriters: Collection<TypeWriter<*>?>, private val basePackages: Collection<Map<Class<*>, String>>) { + private var typeToName: Map<TypeWriter<*>, String> + private val typesWithFactories = THashSet<TypeWriter<*>>() + val typesWithFactoriesList = ArrayList<TypeWriter<*>>(); + + init { + var uniqueCode = 0 + val result = THashMap<TypeWriter<*>, String>(typeWriters.size) + for (handler in typeWriters) { + val conflict = result.put(handler, "M${Integer.toString(uniqueCode++, Character.MAX_RADIX)}") + if (conflict != null) { + throw RuntimeException() + } + } + typeToName = result + } + + fun getTypeImplReference(typeWriter: TypeWriter<*>): String { + val localName = typeToName.get(typeWriter) + if (localName != null) { + return localName + } + + for (base in basePackages) { + val result = base.get(typeWriter.typeClass) + if (result != null) { + return result + } + } + + throw RuntimeException() + } + + fun requireFactoryGenerationAndGetName(typeWriter: TypeWriter<*>): String { + val name = getTypeImplShortName(typeWriter) + if (typesWithFactories.add(typeWriter)) { + typesWithFactoriesList.add(typeWriter) + } + return name + } + + fun getTypeImplShortName(typeWriter: TypeWriter<*>) = typeToName.get(typeWriter)!! +} diff --git a/platform/script-debugger/protocol/protocol-reader/src/InterfaceReader.kt b/platform/script-debugger/protocol/protocol-reader/src/InterfaceReader.kt new file mode 100644 index 00000000..88162cb7 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader/src/InterfaceReader.kt @@ -0,0 +1,221 @@ +package org.jetbrains.protocolReader + +import gnu.trove.THashSet +import org.jetbrains.io.JsonReaderEx +import org.jetbrains.jsonProtocol.JsonField +import org.jetbrains.jsonProtocol.JsonSubtype +import org.jetbrains.jsonProtocol.Optional +import org.jetbrains.jsonProtocol.StringIntPair +import java.lang.reflect.Method +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import java.lang.reflect.WildcardType +import java.util.* +import kotlin.reflect.KCallable + +internal fun InterfaceReader(protocolInterfaces: List<Class<*>>): InterfaceReader { + val map = LinkedHashMap<Class<*>, TypeWriter<*>?>(protocolInterfaces.size) + for (typeClass in protocolInterfaces) { + map.put(typeClass, null) + } + return InterfaceReader(map) +} + +private val LONG_PARSER = PrimitiveValueReader("Long", "-1L") + +private val INTEGER_PARSER = PrimitiveValueReader("Int", "-1") + +private val BOOLEAN_PARSER = PrimitiveValueReader("Boolean", "false") +private val FLOAT_PARSER = PrimitiveValueReader("Float") + +private val NUMBER_PARSER = PrimitiveValueReader("Double", "Double.NaN") + +private val STRING_PARSER = PrimitiveValueReader("String") +private val NULLABLE_STRING_PARSER = PrimitiveValueReader(className = "String", nullable = true) + +private val RAW_STRING_PARSER = PrimitiveValueReader("String", null, true) +private val RAW_STRING_OR_MAP_PARSER = object : PrimitiveValueReader("Any", null, true) { + override fun writeReadCode(scope: ClassScope, subtyping: Boolean, out: TextOutput) { + out.append("readRawStringOrMap(") + addReaderParameter(subtyping, out) + out.append(')') + } +} + +private val JSON_PARSER = RawValueReader() + +private val STRING_INT_PAIR_PARSER = StringIntPairValueReader() + +internal val VOID_PARSER: ValueReader = object : ValueReader() { + override fun appendFinishedValueTypeName(out: TextOutput) { + out.append("void") + } + + override fun writeReadCode(scope: ClassScope, subtyping: Boolean, out: TextOutput) { + out.append("null") + } +} + +internal fun createHandler(typeToTypeHandler: LinkedHashMap<Class<*>, TypeWriter<*>?>, aClass: Class<*>): TypeWriter<*> { + val reader = InterfaceReader(typeToTypeHandler) + reader.processed.addAll(typeToTypeHandler.keys) + reader.go(arrayOf(aClass)) + return typeToTypeHandler.get(aClass)!! +} + +internal class InterfaceReader(val typeToTypeHandler: LinkedHashMap<Class<*>, TypeWriter<*>?>) { + val processed = THashSet<Class<*>>() + private val refs = ArrayList<TypeRef<*>>() + val subtypeCasters = ArrayList<SubtypeCaster>() + + fun go(classes: Array<Class<*>> = typeToTypeHandler.keys.toTypedArray()): LinkedHashMap<Class<*>, TypeWriter<*>?> { + for (typeClass in classes) { + createIfNotExists(typeClass) + } + + var hasUnresolved = true + while (hasUnresolved) { + hasUnresolved = false + // refs can be modified - new items can be added + for (i in 0..refs.size - 1) { + val ref: TypeRef<out Any?> = refs.get(i) + val typeClass: Class<out Any?> = ref.typeClass + (ref as TypeRef<Any?>).type = typeToTypeHandler.get(typeClass) as TypeWriter<Any?>? + if (ref.type == null) { + createIfNotExists(typeClass) + hasUnresolved = true + (ref as TypeRef<Any?>).type = typeToTypeHandler.get(typeClass) as TypeWriter<Any?>? ?: throw IllegalStateException() + } + } + } + + for (subtypeCaster in subtypeCasters) { + subtypeCaster.getSubtypeHandler().subtypeAspect?.setSubtypeCaster(subtypeCaster) + } + + return typeToTypeHandler + } + + private fun createIfNotExists(typeClass: Class<*>) { + if (typeClass == Map::class.java || typeClass == List::class.java || !typeClass.isInterface) { + return + } + + if (!processed.add(typeClass)) { + return + } + + typeToTypeHandler.put(typeClass, null) + + for (aClass in typeClass.declaredClasses) { + createIfNotExists(aClass) + } + + if (!typeClass.isInterface) { + throw JsonProtocolModelParseException("Json model type should be interface: ${typeClass.name}") + } + + val fields = FieldProcessor(this, typeClass) + for (method in fields.methodHandlerMap.keys) { + val returnType = method.returnType + if (returnType != typeClass) { + createIfNotExists(returnType) + } + } + + val typeWriter = TypeWriter(typeClass, getSuperclassRef(typeClass), fields.volatileFields, fields.methodHandlerMap, fields.fieldLoaders, fields.lazyRead) + for (ref in refs) { + if (ref.typeClass == typeClass) { + assert(ref.type == null) + (ref as TypeRef<Any?>).type = typeWriter as TypeWriter<Any?> + break + } + } + typeToTypeHandler.put(typeClass, typeWriter) + } + + fun getFieldTypeParser(member: KCallable<*>?, type: Type, isSubtyping: Boolean, method: Method?): ValueReader { + if (type is Class<*>) { + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") + return when { + type == java.lang.Long.TYPE -> LONG_PARSER + type == Integer.TYPE || type == Integer::class.java -> INTEGER_PARSER + type == java.lang.Boolean.TYPE -> BOOLEAN_PARSER + type == java.lang.Float.TYPE -> FLOAT_PARSER + type == Number::class.java || type == java.lang.Double.TYPE || type == java.lang.Double::class.java -> NUMBER_PARSER + type == Void.TYPE -> VOID_PARSER + type == String::class.java -> { + if (method != null) { + val jsonField = member?.annotation<JsonField>() + if (jsonField != null && jsonField.allowAnyPrimitiveValue) { + return RAW_STRING_PARSER + } + else if ((member?.returnType?.isMarkedNullable ?: false) || method.getAnnotation<Optional>(Optional::class.java) != null) { + return NULLABLE_STRING_PARSER + } + } + return STRING_PARSER + } + type == Any::class.java -> RAW_STRING_OR_MAP_PARSER + type == JsonReaderEx::class.java -> JSON_PARSER + type == StringIntPair::class.java -> STRING_INT_PAIR_PARSER + type.isArray -> ArrayReader(getFieldTypeParser(null, type.componentType, false, null), false) + type.isEnum -> EnumReader(type as Class<Enum<*>>) + else -> { + val ref = getTypeRef(type) ?: throw UnsupportedOperationException("Method return type $type (simple class) not supported") + ObjectValueReader(ref, isSubtyping, method?.getAnnotation<JsonField>(JsonField::class.java)?.primitiveValue) + } + } + } + else if (type is ParameterizedType) { + val isList = type.rawType == List::class.java + if (isList || type.rawType == Map::class.java) { + var argumentType = type.actualTypeArguments[if (isList) 0 else 1] + if (argumentType is WildcardType) { + val wildcard = argumentType + if (wildcard.lowerBounds.size == 0 && wildcard.upperBounds.size == 1) { + argumentType = wildcard.upperBounds[0] + } + } + val componentParser = getFieldTypeParser(null, argumentType, false, method) + return if (isList) ArrayReader(componentParser, true) else MapReader(componentParser) + } + else { + throw UnsupportedOperationException("Method return type $type (generic) not supported") + } + } + else { + throw UnsupportedOperationException("Method return type $type not supported") + } + } + + fun <T> getTypeRef(typeClass: Class<T>): TypeRef<T>? { + val result = TypeRef(typeClass) + refs.add(result) + return result + } + + private fun getSuperclassRef(typeClass: Class<*>): TypeRef<*>? { + var result: TypeRef<*>? = null + for (interfaceGeneric in typeClass.genericInterfaces) { + if (interfaceGeneric !is ParameterizedType) { + continue + } + if (interfaceGeneric.rawType != JsonSubtype::class.java) { + continue + } + val param = interfaceGeneric.actualTypeArguments[0] + if (param !is Class<*>) { + throw JsonProtocolModelParseException("Unexpected type of superclass $param") + } + if (result != null) { + throw JsonProtocolModelParseException("Already has superclass ${result.typeClass.name}") + } + result = getTypeRef(param) + if (result == null) { + throw JsonProtocolModelParseException("Unknown base class ${param.name}") + } + } + return result + } +}
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-reader/src/JsonProtocolModelParseException.kt b/platform/script-debugger/protocol/protocol-reader/src/JsonProtocolModelParseException.kt new file mode 100644 index 00000000..5007fd49 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader/src/JsonProtocolModelParseException.kt @@ -0,0 +1,8 @@ +package org.jetbrains.protocolReader + +/** + * Signals that JSON model has some problem in it. + */ +fun JsonProtocolModelParseException(message: String): JsonProtocolModelParseException = JsonProtocolModelParseException(message, null) + +class JsonProtocolModelParseException(message: String, cause: Throwable?) : RuntimeException(message, cause) diff --git a/platform/script-debugger/protocol/protocol-reader/src/LazyCachedMethodHandler.kt b/platform/script-debugger/protocol/protocol-reader/src/LazyCachedMethodHandler.kt new file mode 100644 index 00000000..3e51e1aa --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader/src/LazyCachedMethodHandler.kt @@ -0,0 +1,51 @@ +package org.jetbrains.protocolReader + +import java.lang.reflect.Method + +/** + * Basic implementation of the method that parses value on demand and store it for a future use + */ +internal class LazyCachedMethodHandler(private val parser: ValueReader, private val fieldBinding: VolatileFieldBinding) : MethodHandler { + protected fun writeReturnTypeJava(scope: ClassScope, m: Method, out: TextOutput) { + val objectValueParser = parser.asJsonTypeParser() + if (objectValueParser == null) { + writeJavaTypeName(m.genericReturnType, out) + } + else { + out.append(scope.getTypeImplReference(objectValueParser.type.type!!)) + } + } + + override fun writeMethodImplementationJava(scope: ClassScope, method: Method, out: TextOutput) { + out.append("override fun ") + appendMethodSignatureJava(method, listOf(), out) + out.append(": ") + writeReturnTypeJava(scope, method, out) + + out.openBlock() + out.append("if (") + fieldBinding.writeGetExpression(out) + out.append(" == null)").block { + if (parser.isThrowsIOException()) { + out.append("try").openBlock() + } + + fieldBinding.writeGetExpression(out) + out.append(" = ") + parser.writeReadCode(scope, true, scope.output) + + if (parser.isThrowsIOException()) { + out.closeBlock() + out.newLine().append("catch (e: IOException)").openBlock() + out.append("throw com.google.gson.JsonParseException(e)").closeBlock() + } + out.newLine().append(PENDING_INPUT_READER_NAME).append(" = null") + } + + out.newLine().append("return ") + fieldBinding.writeGetExpression(out) + out.append("!!") + + out.closeBlock() + } +} diff --git a/platform/script-debugger/protocol/protocol-reader/src/MapReader.kt b/platform/script-debugger/protocol/protocol-reader/src/MapReader.kt new file mode 100644 index 00000000..d251d92a --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader/src/MapReader.kt @@ -0,0 +1,34 @@ +package org.jetbrains.protocolReader + +internal class MapReader(private val componentParser: ValueReader) : ValueReader() { + override fun appendFinishedValueTypeName(out: TextOutput) { + out.append("Map") + out.append('<') + out.append("String, ") + componentParser.appendFinishedValueTypeName(out) + out.append('>') + } + + override fun writeReadCode(scope: ClassScope, subtyping: Boolean, out: TextOutput) { + beginReadCall("Map", subtyping, out) + if (componentParser is ObjectValueReader) { + componentParser.writeFactoryArgument(scope, out) + } + else { + out.comma().append("null") + } + out.append(')') + } + + override fun writeArrayReadCode(scope: ClassScope, subtyping: Boolean, out: TextOutput) { + beginReadCall("ObjectArray", subtyping, out) + out.comma().append("mapFactory(") + if (componentParser is ObjectValueReader) { + componentParser.writeFactoryNewExpression(scope, out) + } + else { + out.append("STRING_OBJECT_FACTORY") + } + out.append("))") + } +} diff --git a/platform/script-debugger/protocol/protocol-reader/src/MethodHandler.kt b/platform/script-debugger/protocol/protocol-reader/src/MethodHandler.kt new file mode 100644 index 00000000..f0dd55c6 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader/src/MethodHandler.kt @@ -0,0 +1,31 @@ +package org.jetbrains.protocolReader + +import java.lang.reflect.Method + +internal fun appendMethodSignatureJava(method: Method, paramNames: List<String>, out: TextOutput) { + out.append(method.name).append('(') + var firstArg = true + val types = method.genericParameterTypes + for (i in 0..types.size - 1) { + val arg = types[i] + if (firstArg) { + firstArg = false + } + else { + out.comma() + } + out.append(paramNames.get(i)) + out.append(": ") + writeJavaTypeName(arg, out) + } + out.append(')') +} + +fun writeMethodDeclarationJava(out: TextOutput, m: Method, paramNames: List<String> = listOf<String>()) { + out.append("override fun ") + appendMethodSignatureJava(m, paramNames, out) +} + +internal interface MethodHandler { + fun writeMethodImplementationJava(scope: ClassScope, method: Method, out: TextOutput) +} diff --git a/platform/script-debugger/protocol/protocol-reader/src/ObjectValueReader.kt b/platform/script-debugger/protocol/protocol-reader/src/ObjectValueReader.kt new file mode 100644 index 00000000..ef6e02f2 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader/src/ObjectValueReader.kt @@ -0,0 +1,43 @@ +package org.jetbrains.protocolReader + +internal class ObjectValueReader(val type: TypeRef<*>, private val isSubtyping: Boolean, primitiveValueName: String?) : ValueReader() { + val primitiveValueName = if (primitiveValueName == null || primitiveValueName.isEmpty()) null else primitiveValueName + + override fun asJsonTypeParser() = this + + fun isSubtyping() = isSubtyping + + override fun appendFinishedValueTypeName(out: TextOutput) { + out.append(type.typeClass.canonicalName) + } + + override fun appendInternalValueTypeName(scope: FileScope, out: TextOutput) { + out.append(scope.getTypeImplReference(type.type!!)) + } + + override fun writeReadCode(scope: ClassScope, subtyping: Boolean, out: TextOutput) { + type.type!!.writeInstantiateCode(scope.getRootClassScope(), subtyping, out) + out.append('(') + addReaderParameter(subtyping, out) + out.comma().append("null") + if (subtyping && type.type!!.subtypeAspect != null) { + out.comma().append("this") + } + out.append(')') + } + + override fun writeArrayReadCode(scope: ClassScope, subtyping: Boolean, out: TextOutput) { + beginReadCall("ObjectArray", subtyping, out) + writeFactoryArgument(scope, out) + out.append(')') + } + + fun writeFactoryArgument(scope: ClassScope, out: TextOutput) { + out.comma() + writeFactoryNewExpression(scope, out) + } + + fun writeFactoryNewExpression(scope: ClassScope, out: TextOutput) { + out.append(TYPE_FACTORY_NAME_PREFIX).append(scope.requireFactoryGenerationAndGetName(type.type!!)).append("()") + } +} diff --git a/platform/script-debugger/protocol/protocol-reader/src/PrimitiveValueReader.kt b/platform/script-debugger/protocol/protocol-reader/src/PrimitiveValueReader.kt new file mode 100644 index 00000000..3a126108 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader/src/PrimitiveValueReader.kt @@ -0,0 +1,44 @@ +package org.jetbrains.protocolReader + +internal open class PrimitiveValueReader(val className: String, val defaultValue: String? = null, private val asRawString: Boolean = false, private val nullable: Boolean = false) : ValueReader() { + private val readPostfix: String + + init { + if (Character.isLowerCase(className.get(0))) { + readPostfix = "${Character.toUpperCase(className.get(0))}${className.substring(1)}" + } + else { + readPostfix = if (asRawString) ("Raw$className") else className + } + } + + override fun writeReadCode(scope: ClassScope, subtyping: Boolean, out: TextOutput) { + if (asRawString) { + out.append("readRawString(") + addReaderParameter(subtyping, out) + out.append(')') + } + else { + addReaderParameter(subtyping, out) + out.append(".next"); + if (nullable) { + out.append("Nullable"); + } + out.append(readPostfix).append("()") + } + } + + override fun appendFinishedValueTypeName(out: TextOutput) { + out.append(className) + } + + override fun writeArrayReadCode(scope: ClassScope, subtyping: Boolean, out: TextOutput) { + if (readPostfix == "String") { + out.append("nextList") + } + else { + out.append("read").append(readPostfix).append("Array") + } + out.append('(').append(READER_NAME).append(')') + } +} diff --git a/platform/script-debugger/protocol/protocol-reader/src/RawValueReader.kt b/platform/script-debugger/protocol/protocol-reader/src/RawValueReader.kt new file mode 100644 index 00000000..88a3ecda --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader/src/RawValueReader.kt @@ -0,0 +1,14 @@ +package org.jetbrains.protocolReader + +import org.jetbrains.io.JsonReaderEx + +internal class RawValueReader() : ValueReader() { + override fun writeReadCode(scope: ClassScope, subtyping: Boolean, out: TextOutput) { + addReaderParameter(subtyping, out) + out.append(".createSubReaderAndSkipValue()") + } + + override fun appendFinishedValueTypeName(out: TextOutput) { + out.append(JsonReaderEx::class.java.canonicalName) + } +}
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-reader/src/ReaderGenerator.kt b/platform/script-debugger/protocol/protocol-reader/src/ReaderGenerator.kt new file mode 100644 index 00000000..7397d6fa --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader/src/ReaderGenerator.kt @@ -0,0 +1,117 @@ +package org.jetbrains.protocolReader + +import gnu.trove.THashMap +import java.nio.file.Paths +import java.util.* + +class GenerateConfiguration<ROOT>(val packageName: String, val className: String, readerRootClass: Class<ROOT>, protocolInterfaces: List<Class<*>>, basePackagesMap: Map<Class<*>, String>? = null) { + val basePackagesMap: List<Map<Class<*>, String>> = if (basePackagesMap == null) listOf<Map<Class<*>, String>>() else listOf(basePackagesMap) + + internal val typeToTypeHandler = InterfaceReader(protocolInterfaces).go() + internal val root = ReaderRoot(readerRootClass, typeToTypeHandler) +} + +fun generate(args: Array<String>, configuration: GenerateConfiguration<*>) { + val fileUpdater = FileUpdater(Paths.get(parseArgs(args), "${configuration.className}.kt")) + generate(configuration, fileUpdater.builder) + fileUpdater.update() +} + +private fun parseArgs(args: Array<String>): String { + val outputDirParam = StringParam() + + val paramMap = HashMap<String, StringParam>(3) + paramMap.put("output-dir", outputDirParam) + + for (arg in args) { + if (!arg.startsWith("--")) { + throw IllegalArgumentException("Unrecognized param: $arg") + } + val equalsPos = arg.indexOf('=', 2) + val key: String + val value: String? + if (equalsPos == -1) { + key = arg.substring(2).trim() + value = null + } + else { + key = arg.substring(2, equalsPos).trim() + value = arg.substring(equalsPos + 1).trim() + } + val paramListener = paramMap.get(key) ?: throw IllegalArgumentException("Unrecognized param name: $key") + try { + paramListener.value = value + } + catch (e: IllegalArgumentException) { + throw IllegalArgumentException("Failed to set value of $key", e) + } + + } + for (en in paramMap.entries) { + if (en.value.value == null) { + en.value.value = "generated" + } + } + + return outputDirParam.value!! +} + +private class StringParam { + var value: String? = null +} + +fun buildParserMap(configuration: GenerateConfiguration<*>): Map<Class<*>, String> { + val fileScope = generate(configuration, StringBuilder()) + + val typeToImplClassName = THashMap<Class<*>, String>() + for (typeWriter in configuration.typeToTypeHandler.values) { + typeToImplClassName.put(typeWriter!!.typeClass, "${configuration.packageName}.${configuration.className}.${fileScope.getTypeImplShortName(typeWriter)}") + } + return typeToImplClassName +} + +private fun generate(configuration: GenerateConfiguration<*>, stringBuilder: StringBuilder): FileScope { + val globalScope = GlobalScope(configuration.typeToTypeHandler.values, configuration.basePackagesMap) + val fileScope = globalScope.newFileScope(stringBuilder) + val out = fileScope.output + out.append("// Generated source") + out.newLine().append("package ").append(configuration.packageName) + out.newLine().newLine().append("import org.jetbrains.jsonProtocol.*") + + out.newLine() + out.newLine().append("import org.jetbrains.io.JsonReaderEx") + + out.newLine().newLine().append("import org.jetbrains.jsonProtocol.JsonReaders.*") + out.newLine().newLine().append("internal class ").append(configuration.className).space() + out.append(':').space().append(configuration.root.type.canonicalName).append(if (configuration.root.type.isInterface) "" else "()").openBlock(false) + + val rootClassScope = fileScope.newClassScope() + configuration.root.write(rootClassScope) + + for (typeWriter in configuration.typeToTypeHandler.values) { + out.newLine() + typeWriter!!.write(rootClassScope) + out.newLine() + } + + var isFirst = true + for (typeWriter in globalScope.getTypeFactories()) { + if (isFirst) { + isFirst = false + } + else { + out.newLine() + } + + val originName = typeWriter.typeClass.canonicalName + out.newLine().append("private class ").append(TYPE_FACTORY_NAME_PREFIX).append(globalScope.getTypeImplShortName(typeWriter)).append(" : ObjectFactory<") + out.append(originName).append(">()").openBlock() + out.append("override fun read(").append(JSON_READER_PARAMETER_DEF).append("): ").append(originName).append(" = ") + typeWriter.writeInstantiateCode(rootClassScope, out) + out.append('(').append(READER_NAME).append(", null)") + out.closeBlock() + } + + out.closeBlock() + return fileScope +} diff --git a/platform/script-debugger/protocol/protocol-reader/src/ReaderRoot.kt b/platform/script-debugger/protocol/protocol-reader/src/ReaderRoot.kt new file mode 100644 index 00000000..c00abbe3 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader/src/ReaderRoot.kt @@ -0,0 +1,105 @@ +package org.jetbrains.protocolReader + +import gnu.trove.THashSet +import org.jetbrains.io.JsonReaderEx +import org.jetbrains.jsonProtocol.JsonParseMethod +import java.lang.reflect.Method +import java.lang.reflect.ParameterizedType +import java.util.* + +internal class ReaderRoot<R>(val type: Class<R>, private val typeToTypeHandler: LinkedHashMap<Class<*>, TypeWriter<*>?>) { + private val visitedInterfaces = THashSet<Class<*>>(1) + val methodMap = LinkedHashMap<Method, ReadDelegate>() + + init { + readInterfaceRecursive(type) + } + + private fun readInterfaceRecursive(clazz: Class<*>) { + if (visitedInterfaces.contains(clazz)) { + return + } + visitedInterfaces.add(clazz) + + // todo sort by source location + val methods = clazz.methods + Arrays.sort<Method>(methods, { o1, o2 -> o1.name.compareTo(o2.name) }) + + for (m in methods) { + m.getAnnotation<JsonParseMethod>(JsonParseMethod::class.java) ?: continue + + val exceptionTypes = m.exceptionTypes + if (exceptionTypes.size > 1) { + throw JsonProtocolModelParseException("Too many exception declared in $m") + } + + var returnType = m.genericReturnType + var isList = false + if (returnType is ParameterizedType) { + val parameterizedType = returnType + if (parameterizedType.rawType == List::class.java) { + isList = true + returnType = parameterizedType.actualTypeArguments[0] + } + } + + //noinspection SuspiciousMethodCalls + var typeWriter: TypeWriter<*>? = typeToTypeHandler.get(returnType as Any?) + if (typeWriter == null) { + typeWriter = createHandler(typeToTypeHandler, m.returnType) + } + + val arguments = m.genericParameterTypes + if (arguments.size > 2) { + throw JsonProtocolModelParseException("Exactly one argument is expected in $m") + } + val argument = arguments[0] + if (argument == JsonReaderEx::class.java || argument == Any::class.java) { + methodMap.put(m, ReadDelegate(typeWriter, isList, arguments.size != 1)) + } + else { + throw JsonProtocolModelParseException("Unrecognized argument type in $m") + } + } + + for (baseType in clazz.genericInterfaces) { + if (baseType !is Class<*>) { + throw JsonProtocolModelParseException("Base interface must be class in $clazz") + } + readInterfaceRecursive(baseType) + } + } + + fun write(scope: ClassScope) { + val out = scope.output + for (entry in methodMap.entries) { + out.newLine() + entry.value.write(scope, entry.key, out) + out.newLine() + } + } +} + +private val STATIC_METHOD_PARAM_NAME_LIST = listOf(READER_NAME) +private val STATIC_METHOD_PARAM_NAME_LIST2 = Arrays.asList(READER_NAME, "nextName") + +internal class ReadDelegate(private val typeHandler: TypeWriter<*>, private val isList: Boolean, hasNextNameParam: Boolean) { + private val paramNames = if (hasNextNameParam) STATIC_METHOD_PARAM_NAME_LIST2 else STATIC_METHOD_PARAM_NAME_LIST + + fun write(scope: ClassScope, method: Method, out: TextOutput) { + writeMethodDeclarationJava(out, method, paramNames) + out.append(": ") + writeJavaTypeName(method.genericReturnType, out) + out.append(" = ") + if (isList) { + out.append("readObjectArray(").append(READER_NAME).append(", ").append(TYPE_FACTORY_NAME_PREFIX).append(scope.requireFactoryGenerationAndGetName(typeHandler)).append("()").append(")") + } + else { + typeHandler.writeInstantiateCode(scope, out) + out.append('(').append(READER_NAME) + out.comma().space() + out.append(if (paramNames.size == 1) "null" else "nextName") + out.append(')') + } + } +}
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-reader/src/StringIntPairValueReader.kt b/platform/script-debugger/protocol/protocol-reader/src/StringIntPairValueReader.kt new file mode 100644 index 00000000..c72976fe --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader/src/StringIntPairValueReader.kt @@ -0,0 +1,16 @@ +package org.jetbrains.protocolReader + +internal class StringIntPairValueReader : ValueReader() { + override fun appendFinishedValueTypeName(out: TextOutput) { + out.append("StringIntPair") + } + + override fun writeReadCode(scope: ClassScope, subtyping: Boolean, out: TextOutput) { + } + + override fun writeArrayReadCode(scope: ClassScope, subtyping: Boolean, out: TextOutput) { + out.append("read").append("IntStringPairs").append('(') + addReaderParameter(subtyping, out) + out.append(')') + } +} diff --git a/platform/script-debugger/protocol/protocol-reader/src/TextOutput.kt b/platform/script-debugger/protocol/protocol-reader/src/TextOutput.kt new file mode 100644 index 00000000..fc7e8277 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader/src/TextOutput.kt @@ -0,0 +1,137 @@ +package org.jetbrains.protocolReader + +import java.util.* + +val EMPTY_CHARS: CharArray = CharArray(0) +private val indentGranularity = 2 + +class TextOutput(val out: StringBuilder) { + private var identLevel: Int = 0 + private var indents = arrayOf(EMPTY_CHARS) + private var justNewLined: Boolean = false + + fun indentIn(): TextOutput { + ++identLevel + if (identLevel >= indents.size) { + // Cache a new level of indentation string. + val newIndentLevel = CharArray(identLevel * indentGranularity) + Arrays.fill(newIndentLevel, ' ') + val newIndents = arrayOfNulls<CharArray>(indents.size + 1) + System.arraycopy(indents, 0, newIndents, 0, indents.size) + newIndents[identLevel] = newIndentLevel + indents = newIndents as Array<CharArray> + } + return this + } + + fun indentOut(): TextOutput { + --identLevel + return this + } + + fun newLine(): TextOutput { + out.append('\n') + justNewLined = true + return this + } + + fun append(value: Double): TextOutput { + maybeIndent() + out.append(value) + return this + } + + fun append(value: Boolean): TextOutput { + maybeIndent() + out.append(value) + return this + } + + fun append(value: Int): TextOutput { + maybeIndent() + out.append(value) + return this + } + + fun append(c: Char): TextOutput { + maybeIndent() + out.append(c) + return this + } + + fun append(s: CharArray) { + maybeIndent() + out.append(s) + } + + fun append(s: CharSequence): TextOutput { + maybeIndent() + out.append(s) + return this + } + + fun append(s: CharSequence, start: Int): TextOutput { + maybeIndent() + out.append(s, start, s.length) + return this + } + + fun openBlock(): TextOutput { + return openBlock(true) + } + + inline fun block(addNewLine: Boolean = true, writer: () -> Unit): TextOutput { + openBlock(addNewLine) + writer() + closeBlock() + return this + } + + fun openBlock(addNewLine: Boolean): TextOutput { + space().append('{') + if (addNewLine) { + newLine() + } + indentIn() + return this + } + + fun closeBlock(): TextOutput { + indentOut().newLine().append('}') + return this + } + + fun comma(): TextOutput = append(',').space() + + fun space(): TextOutput = append(' ') + + fun doc(description: String?): TextOutput { + if (description == null) { + return this + } + return append("/**").newLine().append(" * ").append(description).newLine().append(" */").newLine() + } + + fun quote(s: CharSequence): TextOutput = append('"').append(s).append('"') + + fun maybeIndent() { + if (justNewLined) { + out.append(indents[identLevel]) + justNewLined = false + } + } + + fun appendEscapedName(name: String): TextOutput { + val isEscapeName = name == "object" || name == "fun" + if (isEscapeName) { + out.append('`') + } + out.append(name) + if (isEscapeName) { + out.append('`') + } + return this + } + + operator fun plus(value: String): TextOutput = append(value) +} diff --git a/platform/script-debugger/protocol/protocol-reader/src/TypeWriter.kt b/platform/script-debugger/protocol/protocol-reader/src/TypeWriter.kt new file mode 100644 index 00000000..ddf40950 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader/src/TypeWriter.kt @@ -0,0 +1,258 @@ +package org.jetbrains.protocolReader + +import org.jetbrains.jsonProtocol.JsonObjectBased +import java.lang.reflect.Method +import java.util.* + +internal val FIELD_PREFIX = '_' + +internal val NAME_VAR_NAME = "_n" + +private fun assignField(out: TextOutput, fieldName: String) = out.append(FIELD_PREFIX).append(fieldName).append(" = ") + +internal class TypeRef<T>(val typeClass: Class<T>) { + var type: TypeWriter<T>? = null +} + +internal class TypeWriter<T>(val typeClass: Class<T>, jsonSuperClass: TypeRef<*>?, private val volatileFields: List<VolatileFieldBinding>, private val methodHandlerMap: LinkedHashMap<Method, MethodHandler>, + /** Loaders that should read values and save them in field array on parse time. */ + private val fieldLoaders: List<FieldLoader>, private val hasLazyFields: Boolean) { + + /** Subtype aspects of the type or null */ + val subtypeAspect = if (jsonSuperClass == null) null else ExistingSubtypeAspect(jsonSuperClass) + + fun writeInstantiateCode(scope: ClassScope, out: TextOutput) { + writeInstantiateCode(scope, false, out) + } + + fun writeInstantiateCode(scope: ClassScope, deferredReading: Boolean, out: TextOutput) { + val className = scope.getTypeImplReference(this) + if (deferredReading || subtypeAspect == null) { + out.append(className) + } + else { + subtypeAspect.writeInstantiateCode(className, out) + } + } + + fun write(fileScope: FileScope) { + val out = fileScope.output + val valueImplClassName = fileScope.getTypeImplShortName(this) + out.append("private class ").append(valueImplClassName).append('(').append(JSON_READER_PARAMETER_DEF).comma().append("preReadName: String?") + subtypeAspect?.writeSuperFieldJava(out) + out.append(") : ").append(typeClass.canonicalName).openBlock() + + if (hasLazyFields || JsonObjectBased::class.java.isAssignableFrom(typeClass)) { + out.append("private var ").append(PENDING_INPUT_READER_NAME).append(": ").append(JSON_READER_CLASS_NAME).append("? = reader.subReader()!!").newLine() + } + + val classScope = fileScope.newClassScope() + for (field in volatileFields) { + field.writeFieldDeclaration(classScope, out) + out.newLine() + } + + for (loader in fieldLoaders) { + if (loader.asImpl) { + out.append("override") + } + else { + out.append("private") + } + out.append(" var ").appendName(loader) + + fun addType() { + out.append(": ") + loader.valueReader.appendFinishedValueTypeName(out) + out.append("? = null") + } + + if (loader.valueReader is PrimitiveValueReader) { + val defaultValue = loader.defaultValue ?: loader.valueReader.defaultValue + if (defaultValue != null) { + out.append(" = ").append(defaultValue) + } + else { + addType() + } + } + else { + addType() + } + out.newLine() + } + + if (fieldLoaders.isNotEmpty()) { + out.newLine() + } + writeConstructorMethod(classScope, out) + out.newLine() + + subtypeAspect?.writeParseMethod(classScope, out) + + for ((key, value) in methodHandlerMap.entries) { + out.newLine() + value.writeMethodImplementationJava(classScope, key, out) + out.newLine() + } + + writeBaseMethods(out) + subtypeAspect?.writeGetSuperMethodJava(out) + + writeEqualsMethod(valueImplClassName, out) + + out.indentOut().append('}') + } + + /** + * Generates Java implementation of standard methods of JSON type class (if needed): + * {@link org.jetbrains.jsonProtocol.JsonObjectBased#getDeferredReader()} + */ + private fun writeBaseMethods(out: TextOutput) { + val method: Method + try { + method = typeClass.getMethod("getDeferredReader") + } + catch (ignored: NoSuchMethodException) { + // Method not found, skip. + return + } + + out.newLine() + writeMethodDeclarationJava(out, method) + out.append(" = ").append(PENDING_INPUT_READER_NAME) + } + + private fun writeEqualsMethod(valueImplClassName: String, out: TextOutput) { + if (fieldLoaders.isEmpty()) { + return + } + + out.newLine().append("override fun equals(other: Any?): Boolean = ") + out.append("other is ").append(valueImplClassName) + + // at first we should compare primitive values, then enums, then string, then objects + fun fieldWeight(reader: ValueReader): Int { + var w = 10 + if (reader is PrimitiveValueReader) { + w-- + if (reader.className != "String") { + w-- + } + } + else if (reader is EnumReader) { + // -1 as primitive, -1 as not a string + w -= 2 + } + return w + } + + for (loader in fieldLoaders.sortedWith(Comparator<FieldLoader> { f1, f2 -> fieldWeight((f1.valueReader)) - fieldWeight((f2.valueReader))})) { + out.append(" && ") + out.appendName(loader).append(" == ").append("other.").appendName(loader) + } + out.newLine() + } + + private fun writeConstructorMethod(classScope: ClassScope, out: TextOutput) { + out.append("init").block { + if (fieldLoaders.isEmpty()) { + out.append(READER_NAME).append(".skipValue()") + } + else { + out.append("var ").append(NAME_VAR_NAME).append(" = preReadName") + out.newLine().append("if (").append(NAME_VAR_NAME).append(" == null && reader.hasNext() && reader.beginObject().hasNext())").block { + out.append(NAME_VAR_NAME).append(" = reader.nextName()") + } + out.newLine() + + writeReadFields(out, classScope) + + // we don't read all data if we have lazy fields, so, we should not check end of stream + //if (!hasLazyFields) { + out.newLine().newLine().append(READER_NAME).append(".endObject()") + //} + } + } + } + + private fun writeReadFields(out: TextOutput, classScope: ClassScope) { + val stopIfAllFieldsWereRead = hasLazyFields + val hasOnlyOneFieldLoader = fieldLoaders.size == 1 + val isTracedStop = stopIfAllFieldsWereRead && !hasOnlyOneFieldLoader + if (isTracedStop) { + out.newLine().append("var i = 0") + } + + out.newLine().append("loop@ while (").append(NAME_VAR_NAME).append(" != null)").block { + (out + "when (" + NAME_VAR_NAME + ")").block { + var isFirst = true + for (fieldLoader in fieldLoaders) { + if (fieldLoader.skipRead) { + continue + } + + if (!isFirst) { + out.newLine() + } + + out.append('"') + if (fieldLoader.jsonName.first() == '$') { + out.append('\\') + } + out.append(fieldLoader.jsonName).append('"').append(" -> ") + + if (stopIfAllFieldsWereRead && !isTracedStop) { + out.openBlock() + } + + val primitiveValueName = if (fieldLoader.valueReader is ObjectValueReader) fieldLoader.valueReader.primitiveValueName else null + if (primitiveValueName != null) { + out.append("if (reader.peek() == com.google.gson.stream.JsonToken.BEGIN_OBJECT)").openBlock() + } + out.appendName(fieldLoader).append(" = ") + + fieldLoader.valueReader.writeReadCode(classScope, false, out) + + if (primitiveValueName != null) { + out.closeBlock().newLine().append("else").block { + assignField(out, "${primitiveValueName}Type") + out.append("reader.peek()").newLine() + + assignField(out, primitiveValueName) + out + "reader.nextString(true)" + } + } + + if (stopIfAllFieldsWereRead && !isTracedStop) { + out.newLine().append(READER_NAME).append(".skipValues()").newLine().append("break@loop").closeBlock() + } + + if (isFirst) { + isFirst = false + } + } + + out.newLine().append("else ->") + if (isTracedStop) { + out.block { + out.append("reader.skipValue()") + out.newLine() + NAME_VAR_NAME + " = reader.nextNameOrNull()" + out.newLine() + "continue@loop" + } + } + else { + out.space().append("reader.skipValue()") + } + } + + out.newLine() + NAME_VAR_NAME + " = reader.nextNameOrNull()" + + if (isTracedStop) { + out.newLine().newLine().append("if (i++ == ").append(fieldLoaders.size - 1).append(")").block { + (out + READER_NAME + ".skipValues()").newLine() + "break" + } + } + } + } +}
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-reader/src/Util.kt b/platform/script-debugger/protocol/protocol-reader/src/Util.kt new file mode 100644 index 00000000..994ae5e1 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader/src/Util.kt @@ -0,0 +1,50 @@ +package org.jetbrains.protocolReader + +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import java.lang.reflect.WildcardType + +val TYPE_FACTORY_NAME_PREFIX: Char = 'F' + +val READER_NAME: String = "reader" +val PENDING_INPUT_READER_NAME: String = "inputReader" + +val JSON_READER_CLASS_NAME: String = "JsonReaderEx" +val JSON_READER_PARAMETER_DEF: String = "$READER_NAME: $JSON_READER_CLASS_NAME" + +/** + * Generate Java type name of the passed type. Type may be parameterized. + */ +internal fun writeJavaTypeName(arg: Type, out: TextOutput) { + if (arg is Class<*>) { + val name = arg.canonicalName + out.append( + if (name == "java.util.List") "List" + else if (name == "java.lang.String") "String?" + else name + ) + } + else if (arg is ParameterizedType) { + writeJavaTypeName(arg.rawType, out) + out.append('<') + val params = arg.actualTypeArguments + for (i in params.indices) { + if (i != 0) { + out.comma() + } + writeJavaTypeName(params[i], out) + } + out.append('>') + } + else if (arg is WildcardType) { + val upperBounds = arg.upperBounds!! + if (upperBounds.size != 1) { + throw RuntimeException() + } + out.append("? extends ") + writeJavaTypeName(upperBounds.first(), out) + } + else { + out.append(arg.toString()) + } +}
\ No newline at end of file diff --git a/platform/script-debugger/protocol/protocol-reader/src/ValueReader.kt b/platform/script-debugger/protocol/protocol-reader/src/ValueReader.kt new file mode 100644 index 00000000..a291d4e4 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader/src/ValueReader.kt @@ -0,0 +1,38 @@ +package org.jetbrains.protocolReader + +fun addReaderParameter(subtyping: Boolean, out: TextOutput) { + if (subtyping) { + out.append(PENDING_INPUT_READER_NAME).append("!!") + } + else { + out.append(READER_NAME) + } +} + +/** + * A parser that accepts value of JSON field and outputs value in another form (e.g. string + * is converted to enum constant) to serve field getters in JsonType interfaces. + */ +internal abstract class ValueReader { + open fun asJsonTypeParser(): ObjectValueReader? = null + + abstract fun appendFinishedValueTypeName(out: TextOutput) + + open fun appendInternalValueTypeName(scope: FileScope, out: TextOutput) { + appendFinishedValueTypeName(out) + } + + abstract fun writeReadCode(scope: ClassScope, subtyping: Boolean, out: TextOutput) + + open fun writeArrayReadCode(scope: ClassScope, subtyping: Boolean, out: TextOutput) { + throw UnsupportedOperationException() + } + + protected fun beginReadCall(readPostfix: String, subtyping: Boolean, out: TextOutput) { + out.append("read") + out.append(readPostfix).append('(') + addReaderParameter(subtyping, out) + } + + fun isThrowsIOException() = false +} diff --git a/platform/script-debugger/protocol/protocol-reader/src/VolatileFieldBinding.kt b/platform/script-debugger/protocol/protocol-reader/src/VolatileFieldBinding.kt new file mode 100644 index 00000000..1dad7920 --- /dev/null +++ b/platform/script-debugger/protocol/protocol-reader/src/VolatileFieldBinding.kt @@ -0,0 +1,19 @@ +package org.jetbrains.protocolReader + +import java.util.concurrent.atomic.AtomicReferenceArray + +internal class VolatileFieldBinding(private val position: Int, private val fieldTypeInfo: (scope: FileScope, out: TextOutput) -> Unit) { + fun get(atomicReferenceArray: AtomicReferenceArray<Any>) = atomicReferenceArray.get(position) + + fun writeGetExpression(out: TextOutput) { + out.append("lazy_").append(position) + } + + fun writeFieldDeclaration(scope: ClassScope, out: TextOutput) { + out.append("private var ") + writeGetExpression(out) + out.append(": ") + fieldTypeInfo(scope, out) + out.append("? = null") + } +} diff --git a/platform/script-debugger/protocol/schema-reader-generator/intellij.javascript.schemaReaderGenerator.iml b/platform/script-debugger/protocol/schema-reader-generator/intellij.javascript.schemaReaderGenerator.iml new file mode 100644 index 00000000..de0edaa8 --- /dev/null +++ b/platform/script-debugger/protocol/schema-reader-generator/intellij.javascript.schemaReaderGenerator.iml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + <orderEntry type="library" name="gson" level="project" /> + <orderEntry type="module" module-name="intellij.javascript.protocolReader" /> + <orderEntry type="library" name="KotlinJavaRuntime" level="project" /> + <orderEntry type="module" module-name="intellij.platform.ide.impl" /> + </component> +</module>
\ No newline at end of file diff --git a/platform/script-debugger/protocol/schema-reader-generator/src/ProtocolMetaModel.kt b/platform/script-debugger/protocol/schema-reader-generator/src/ProtocolMetaModel.kt new file mode 100644 index 00000000..39cd44db --- /dev/null +++ b/platform/script-debugger/protocol/schema-reader-generator/src/ProtocolMetaModel.kt @@ -0,0 +1,127 @@ +package org.jetbrains.jsonProtocol + +import org.jetbrains.io.JsonReaderEx + +val STRING_TYPE: String = "string" +val INTEGER_TYPE: String = "integer" +val NUMBER_TYPE: String = "number" +val BOOLEAN_TYPE: String = "boolean" +public val OBJECT_TYPE: String = "object" +val ARRAY_TYPE: String = "array" +val UNKNOWN_TYPE: String = "unknown" +val ANY_TYPE: String = "any" + +interface ItemDescriptor { + val description: String? + + val type: String? + + val enum: List<String>? + + val items: ProtocolMetaModel.ArrayItemType? + + interface Named : Referenceable { + fun name(): String + + val shortName: String? + + val optional: Boolean + } + + interface Referenceable : ItemDescriptor { + @ProtocolName("\$ref") + val ref: String? + } + + interface Type : ItemDescriptor { + val properties: List<ProtocolMetaModel.ObjectProperty>? + } +} + +interface ProtocolSchemaReader { + @JsonParseMethod + fun parseRoot(reader: JsonReaderEx): ProtocolMetaModel.Root +} + +/** + * Defines schema of WIP metamodel defined in http://svn.webkit.org/repository/webkit/trunk/Source/WebCore/inspector/Inspector.json + */ +interface ProtocolMetaModel { + @JsonType + interface Root { + val version: Version? + + fun domains(): List<Domain> + } + + interface Version { + fun major(): String + fun minor(): String + } + + interface Domain { + fun domain(): String + + val types: List<StandaloneType>? + + fun commands(): List<Command> + + val events: List<Event>? + + val description: String? + + val hidden: Boolean + + val experimental: Boolean + } + + interface Command { + fun name(): String + + val parameters: List<Parameter>? + + val returns: List<Parameter>? + + val description: String? + + val hidden: Boolean + + val async: Boolean + } + + interface Parameter : ItemDescriptor.Named { + val hidden: Boolean + + @JsonField(allowAnyPrimitiveValue = true) + val default: String? + } + + interface Event { + fun name(): String + + val parameters: List<Parameter>? + + val description: String? + + val hidden: Boolean + + val optionalData: Boolean + } + + @JsonType + interface StandaloneType : ItemDescriptor.Type { + fun id(): String + + val hidden: Boolean + } + + interface ArrayItemType : ItemDescriptor.Type, ItemDescriptor.Referenceable { + val optional: Boolean + } + + interface ObjectProperty : ItemDescriptor.Named { + override fun name(): String + + val hidden: Boolean + } +}
\ No newline at end of file diff --git a/platform/script-debugger/protocol/schema-reader-generator/src/SchemaReaderGenerator.kt b/platform/script-debugger/protocol/schema-reader-generator/src/SchemaReaderGenerator.kt new file mode 100644 index 00000000..06236fbc --- /dev/null +++ b/platform/script-debugger/protocol/schema-reader-generator/src/SchemaReaderGenerator.kt @@ -0,0 +1,12 @@ +package org.jetbrains.jsonProtocol + +import org.jetbrains.protocolReader.GenerateConfiguration +import org.jetbrains.protocolReader.generate + +fun main(args: Array<String>) { + generate(if (args.isEmpty()) arrayOf("--output-dir=community/platform/script-debugger/protocol/protocol-model-generator/generated") else args, + GenerateConfiguration("org.jetbrains.jsonProtocol", + "ProtocolSchemaReaderImpl", + ProtocolSchemaReader::class.java, + ProtocolMetaModel::class.java.declaredClasses.asList())) +}
\ No newline at end of file |