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/debugger-ui |
New upstream version 0~183.5153.4+dfsg
Diffstat (limited to 'platform/script-debugger/debugger-ui')
38 files changed, 3420 insertions, 0 deletions
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 |