diff --git a/build.gradle.kts b/build.gradle.kts index eaaee4bb..d669ecf5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -61,6 +61,7 @@ dependencies { implementation(libs.bundles.junixsocket) implementation(libs.lsp4j) + implementation("org.eclipse.lsp4j:org.eclipse.lsp4j.debug:0.23.1") testImplementation("org.jetbrains.kotlin:kotlin-test-junit") testImplementation(libs.junit) diff --git a/debugger.ps1 b/debugger.ps1 deleted file mode 100644 index a1f2db27..00000000 --- a/debugger.ps1 +++ /dev/null @@ -1,24 +0,0 @@ -Param( - [string]$script, - [array]$bps - ) - -$scriptToDebug = $script - -$breakpointAction = { - Write-Host "Breakpoint hit at line: $($PSItem.Line)" - Write-Host "Script: $($PSItem.Script)" - Write-Host "Stack Trace Details:" - Get-PSCallStack | ForEach-Object { - Write-Host $_ - } -} - -foreach ($breakpointLine in $bps) -{ - Set-PSBreakpoint -Script $scriptToDebug -Line $breakpointLine -Action $breakpointAction -} - -& $scriptToDebug - -Get-PSBreakpoint | Remove-PSBreakpoint diff --git a/src/main/kotlin/com/intellij/plugin/powershell/ide/actions/PowerShellStartDebugAction.kt b/src/main/kotlin/com/intellij/plugin/powershell/ide/actions/PowerShellStartDebugAction.kt deleted file mode 100644 index a47da341..00000000 --- a/src/main/kotlin/com/intellij/plugin/powershell/ide/actions/PowerShellStartDebugAction.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.intellij.plugin.powershell.ide.actions - -import com.intellij.execution.DefaultExecutionResult -import com.intellij.execution.configurations.GeneralCommandLine -import com.intellij.execution.configurations.PtyCommandLine -import com.intellij.execution.process.KillableProcessHandler -import com.intellij.execution.runners.showRunContent -import com.intellij.execution.util.ExecUtil -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.diagnostic.runAndLogException -import com.intellij.openapi.fileEditor.FileEditorManager -import com.intellij.openapi.options.advanced.AdvancedSettings -import com.intellij.openapi.progress.ModalTaskOwner.project -import com.intellij.openapi.project.Project -import com.intellij.openapi.rd.util.withUiContext -import com.intellij.plugin.powershell.ide.run.PowerShellScriptCommandLineState -import com.intellij.plugin.powershell.lang.lsp.LSPInitMain -import com.intellij.psi.PsiDocumentManager -import com.intellij.terminal.TerminalExecutionConsole -import java.nio.charset.Charset - - -class PowerShellStartDebugAction : AnAction(){ - val logger = logger() - - override fun actionPerformed(e: AnActionEvent) { - logger.info("PowerShellStartDebugAction LOG STARTED") - val currentProject: Project = e.project ?: return - var currentDoc = FileEditorManager.getInstance(currentProject).selectedTextEditor?.getDocument() - val psiFile = PsiDocumentManager.getInstance(currentProject).getPsiFile(currentDoc ?: return) - val vFile = psiFile!!.originalFile.virtualFile - val path = vFile.path - val pwshExe = LSPInitMain.getInstance().getPowerShellExecutable() - val commandLine = GeneralCommandLine(pwshExe, "-File", "c:\\debugger.ps1", path, "7") - commandLine.setCharset(Charset.forName("UTF-8")) - commandLine.setWorkDirectory(currentProject.basePath) - val result = ExecUtil.execAndGetOutput(commandLine) - logger.info(result.toString()) - } - - private fun getTerminalCharSet(): Charset { - val name = AdvancedSettings.getString("terminal.character.encoding") - return logger.runAndLogException { charset(name) } ?: Charsets.UTF_8 - } -} - diff --git a/src/main/kotlin/com/intellij/plugin/powershell/ide/debugger/PowerShellBreakpointHandler.kt b/src/main/kotlin/com/intellij/plugin/powershell/ide/debugger/PowerShellBreakpointHandler.kt index 2dc95415..8cebb84b 100644 --- a/src/main/kotlin/com/intellij/plugin/powershell/ide/debugger/PowerShellBreakpointHandler.kt +++ b/src/main/kotlin/com/intellij/plugin/powershell/ide/debugger/PowerShellBreakpointHandler.kt @@ -9,11 +9,14 @@ import com.intellij.xdebugger.breakpoints.XBreakpointHandler import com.intellij.xdebugger.breakpoints.XBreakpointProperties import com.intellij.xdebugger.breakpoints.XBreakpointType import com.intellij.xdebugger.breakpoints.XLineBreakpoint +import com.jetbrains.rd.util.reactive.Signal class PowerShellBreakpointHandler(powerShellDebugProcess: PowerShellDebugProcess, breakpointTypeClass: Class>, *>>): XBreakpointHandler>>( breakpointTypeClass ) { - val myPowerShellDebugProcess = powerShellDebugProcess; + val myPowerShellDebugProcess = powerShellDebugProcess + val registerBreakpointEvent = Signal>>>() + val unregisterBreakpointEvent = Signal>>>() override fun registerBreakpoint(breakpoint: XLineBreakpoint>) { val sourcePosition = breakpoint.sourcePosition @@ -28,6 +31,7 @@ class PowerShellBreakpointHandler(powerShellDebugProcess: PowerShellDebugProcess //myXsltDebugProcess.getSession().setBreakpointInvalid(breakpoint, "Unsupported breakpoint position") return } + registerBreakpointEvent.fire(Pair(fileURL, breakpoint)) /*try { val manager: BreakpointManager = myPowerShellDebugProcess.getBreakpointManager() var bp: Breakpoint @@ -48,8 +52,18 @@ class PowerShellBreakpointHandler(powerShellDebugProcess: PowerShellDebugProcess } override fun unregisterBreakpoint(breakpoint: XLineBreakpoint>, temporary: Boolean) { - TODO("Not yet implemented") - } + val sourcePosition = breakpoint.sourcePosition + if (sourcePosition == null || !sourcePosition.file.exists() || !sourcePosition.file.isValid) { + return + } + val file = sourcePosition.file + val fileURL: String = getFileURL(file) + val lineNumber: Int = breakpoint.line + if (lineNumber == -1) { + //myXsltDebugProcess.getSession().setBreakpointInvalid(breakpoint, "Unsupported breakpoint position") + return + } + unregisterBreakpointEvent.fire(Pair(fileURL, breakpoint)) } fun getFileURL(file: VirtualFile?): String { return VfsUtil.virtualToIoFile(file!!).toURI().toASCIIString() diff --git a/src/main/kotlin/com/intellij/plugin/powershell/ide/debugger/PowerShellBreakpointType.kt b/src/main/kotlin/com/intellij/plugin/powershell/ide/debugger/PowerShellBreakpointType.kt index fda64a3c..f80707b1 100644 --- a/src/main/kotlin/com/intellij/plugin/powershell/ide/debugger/PowerShellBreakpointType.kt +++ b/src/main/kotlin/com/intellij/plugin/powershell/ide/debugger/PowerShellBreakpointType.kt @@ -1,28 +1,109 @@ package com.intellij.plugin.powershell.ide.debugger -import com.intellij.ide.highlighter.XmlFileType +import com.intellij.idea.ActionsBundle +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import com.intellij.plugin.powershell.PowerShellFileType import com.intellij.plugin.powershell.ide.MessagesBundle import com.intellij.psi.PsiDocumentManager +import com.intellij.ui.components.AnActionLink +import com.intellij.ui.components.JBTextField +import com.intellij.ui.components.Label +import com.intellij.util.ui.JBUI +import com.intellij.xdebugger.XDebuggerManager import com.intellij.xdebugger.breakpoints.XBreakpointProperties +import com.intellij.xdebugger.breakpoints.XLineBreakpoint import com.intellij.xdebugger.breakpoints.XLineBreakpointType +import com.intellij.xdebugger.breakpoints.ui.XBreakpointCustomPropertiesPanel +import com.intellij.xdebugger.evaluation.XDebuggerEditorsProvider +import com.intellij.xdebugger.impl.breakpoints.XBreakpointBase +import java.awt.BorderLayout +import java.awt.Component +import java.awt.Dimension +import javax.swing.* -//XsltDebuggerBundle.message("title.xslt.breakpoints") class PowerShellBreakpointType : XLineBreakpointType>("powershell", MessagesBundle.message("powershell.debugger.breakpoints.title")) { override fun canPutAt(file: VirtualFile, line: Int, project: Project): Boolean { val document = FileDocumentManager.getInstance().getDocument(file) ?: return false val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document) ?: return false val fileType = psiFile.fileType - if (fileType != PowerShellFileType.INSTANCE) { - return false - } - return true + return fileType is PowerShellFileType } + override fun createBreakpointProperties(file: VirtualFile, line: Int): XBreakpointProperties<*>? { return null } + + override fun getEditorsProvider( + breakpoint: XLineBreakpoint>, + project: Project + ): XDebuggerEditorsProvider { + return PowerShellDebuggerEditorsProvider(XDebuggerManager.getInstance(project).currentSession, "BPTYPE") + } + + + /*override fun createCustomConditionsPanel(): XBreakpointCustomPropertiesPanel>> { + return CollectionBreakpointPropertiesPanel() + }*/ +} + +class CollectionBreakpointPropertiesPanel : XBreakpointCustomPropertiesPanel>>() { + private var myClsName: String? = null + private var myFieldName: String? = "Condition:" + private var mySaveCollectionHistoryCheckBox: JCheckBox? = null + + override fun getComponent(): JComponent { + + val box = Box.createVerticalBox() + + var panel: JPanel = JBUI.Panels.simplePanel() + //panel.add(JTextField()) + val textField = JBTextField() + val label = Label("Condition:").apply { + labelFor = textField + alignmentX = Component.LEFT_ALIGNMENT + } + box.add(label) + box.add(textField) + panel.add(box) + + return panel + } + + override fun saveTo(breakpoint: XLineBreakpoint>) { + /* val changed = + breakpoint.getProperties().SHOULD_SAVE_COLLECTION_HISTORY !== mySaveCollectionHistoryCheckBox!!.isSelected + breakpoint.getProperties().SHOULD_SAVE_COLLECTION_HISTORY = mySaveCollectionHistoryCheckBox!!.isSelected*/ + /*if (true) { + (breakpoint as XBreakpointBase<*, *, *>).fireBreakpointChanged() + }*/ + } + + override fun loadFrom(breakpoint: XLineBreakpoint>) { + /*val properties = breakpoint.getProperties() + myClsName = properties.myClassName + myFieldName = properties.myFieldName + mySaveCollectionHistoryCheckBox!!.isSelected = properties.SHOULD_SAVE_COLLECTION_HISTORY*/ + } + + private inner class MyShowCollectionHistoryAction : AnAction() { + override fun actionPerformed(e: AnActionEvent) { + val clsName = myClsName + val fieldName = myFieldName + if (clsName == null || fieldName == null) { + return + } + val project = getEventProject(e) ?: return + val session = XDebuggerManager.getInstance(project).currentSession ?: return + //DebuggerUtilsEx.addCollectionHistoryTab(session, clsName, fieldName, null) + } + } + + companion object { + private const val PREFERRED_PANEL_HEIGHT = 40 + } } diff --git a/src/main/kotlin/com/intellij/plugin/powershell/ide/debugger/PowerShellDebugProcess.kt b/src/main/kotlin/com/intellij/plugin/powershell/ide/debugger/PowerShellDebugProcess.kt index 3667e8ed..1d91bdb3 100644 --- a/src/main/kotlin/com/intellij/plugin/powershell/ide/debugger/PowerShellDebugProcess.kt +++ b/src/main/kotlin/com/intellij/plugin/powershell/ide/debugger/PowerShellDebugProcess.kt @@ -3,32 +3,76 @@ package com.intellij.plugin.powershell.ide.debugger import com.intellij.execution.ExecutionResult import com.intellij.execution.ui.ExecutionConsole import com.intellij.openapi.Disposable +import com.intellij.openapi.application.EDT import com.intellij.openapi.util.Key import com.intellij.xdebugger.XDebugProcess import com.intellij.xdebugger.XDebugSession +import com.intellij.xdebugger.XDebuggerManager import com.intellij.xdebugger.breakpoints.XBreakpointHandler import com.intellij.xdebugger.evaluation.XDebuggerEditorsProvider +import com.intellij.xdebugger.frame.XSuspendContext +import com.jetbrains.rd.framework.util.adviseSuspend +import com.jetbrains.rd.util.lifetime.Lifetime +import kotlinx.coroutines.Dispatchers +import org.eclipse.lsp4j.debug.ContinueArguments -class PowerShellDebugProcess(session: XDebugSession, executionResult: ExecutionResult) : XDebugProcess(session), Disposable { +class PowerShellDebugProcess(val xDebugSession: XDebugSession, val executionResult: ExecutionResult, val debuggerManager: XDebuggerManager, val clientSession: PowerShellDebugSession) : XDebugProcess(xDebugSession), Disposable { val KEY: Key = Key.create("com.intellij.plugin.powershell.ide.debugger.PowerShellDebugProcess") - + val myBreakpointHandler = PowerShellBreakpointHandler(this, PowerShellBreakpointType::class.java) val myProcessHandler = executionResult.processHandler init { myProcessHandler.putUserData(KEY, this) } val myExecutionConsole = executionResult.executionConsole - val myEditorsProvider = PowerShellDebuggerEditorsProvider() + val myEditorsProvider = PowerShellDebuggerEditorsProvider(xDebugSession) init { com.intellij.openapi.util.Disposer.register(myExecutionConsole, this) + myBreakpointHandler.registerBreakpointEvent.adviseSuspend(Lifetime.Eternal, Dispatchers.EDT) { + pair -> clientSession.setBreakpoint(pair.first, pair.second) + } + myBreakpointHandler.unregisterBreakpointEvent.adviseSuspend(Lifetime.Eternal, Dispatchers.EDT) { + pair -> clientSession.removeBreakpoint(pair.first, pair.second) + } + } + + private val myXBreakpointHandlers = arrayOf>(myBreakpointHandler) + + override fun resume(context: XSuspendContext?) { + if(context !is PowerShellSuspendContext) { + return + } + clientSession.continueDebugging(context) } - private val myXBreakpointHandlers = arrayOf>( - PowerShellBreakpointHandler( - this, - PowerShellBreakpointType::class.java - ), - ) + override fun stop() { + clientSession.terminateDebugging() + } + + override fun startStepOver(context: XSuspendContext?) { + if(context !is PowerShellSuspendContext) { + return + } + clientSession.startStepOver(context) + } + + override fun startStepInto(context: XSuspendContext?) { + if(context !is PowerShellSuspendContext) { + return + } + clientSession.startStepInto(context) + } + + override fun startStepOut(context: XSuspendContext?) { + if(context !is PowerShellSuspendContext) { + return + } + clientSession.startStepOut(context) + } + + override fun startPausing() { + clientSession.startPausing() + } override fun createConsole(): ExecutionConsole { return myExecutionConsole @@ -42,6 +86,6 @@ class PowerShellDebugProcess(session: XDebugSession, executionResult: ExecutionR } override fun dispose() { - TODO("Not yet implemented") + clientSession.terminateDebugging() } } diff --git a/src/main/kotlin/com/intellij/plugin/powershell/ide/debugger/PowerShellDebugSession.kt b/src/main/kotlin/com/intellij/plugin/powershell/ide/debugger/PowerShellDebugSession.kt new file mode 100644 index 00000000..baddd66a --- /dev/null +++ b/src/main/kotlin/com/intellij/plugin/powershell/ide/debugger/PowerShellDebugSession.kt @@ -0,0 +1,314 @@ +package com.intellij.plugin.powershell.ide.debugger + +import com.intellij.openapi.application.EDT +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.plugin.powershell.lang.debugger.PSDebugClient +import com.intellij.ui.ColoredTextContainer +import com.intellij.ui.IconManager +import com.intellij.ui.PlatformIcons +import com.intellij.ui.SimpleTextAttributes +import com.intellij.util.io.await +import com.intellij.xdebugger.XDebugSession +import com.intellij.xdebugger.XDebuggerUtil +import com.intellij.xdebugger.XExpression +import com.intellij.xdebugger.XSourcePosition +import com.intellij.xdebugger.breakpoints.XBreakpointProperties +import com.intellij.xdebugger.breakpoints.XLineBreakpoint +import com.intellij.xdebugger.evaluation.XDebuggerEvaluator +import com.intellij.xdebugger.frame.* +import com.jetbrains.rd.framework.util.adviseSuspend +import com.jetbrains.rd.util.lifetime.Lifetime +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.apache.xml.resolver.helpers.FileURL +import org.eclipse.lsp4j.debug.* +import org.eclipse.lsp4j.debug.services.IDebugProtocolServer +import java.util.* +import java.util.concurrent.TimeUnit +import javax.swing.Icon +import kotlin.collections.HashMap +import kotlin.io.path.Path + +class PowerShellDebugSession(val client: PSDebugClient, val server: IDebugProtocolServer, val session: XDebugSession, val coroutineScope: CoroutineScope, val xDebugSession: XDebugSession) { + val breakpointMap = mutableMapOf>>>() + + init{ + client.debugStopped.adviseSuspend(Lifetime.Eternal, Dispatchers.EDT){ + args -> + val stack = server.stackTrace(StackTraceArguments().apply { threadId = args!!.threadId }).await() + thisLogger().info(stack.toString()) + session.positionReached(PowerShellSuspendContext(stack, server, coroutineScope, args!!.threadId, xDebugSession)) + } + } + + fun setBreakpoint(fileURL: String, breakpoint: XLineBreakpoint>) { + if(!breakpointMap.containsKey(fileURL)) + breakpointMap[fileURL] = mutableMapOf() + val bpMap = breakpointMap[fileURL]!! + bpMap[breakpoint.line] = breakpoint + + sendBreakpointRequest() + } + + fun removeBreakpoint(fileURL: String, breakpoint: XLineBreakpoint>) { + if(!breakpointMap.containsKey(fileURL)) + breakpointMap[fileURL] = mutableMapOf() + val bpMap = breakpointMap[fileURL]!! + if(bpMap.containsKey(breakpoint.line)) + bpMap.remove(breakpoint.line) + + sendBreakpointRequest() + } + + fun terminateDebugging() + { + coroutineScope.launch { + server.terminate(TerminateArguments()).await() + } + } + + fun restartDebugging() + { + coroutineScope.launch { + server.restart(RestartArguments()).await() + } + } + + fun continueDebugging(context: PowerShellSuspendContext) + { + continueDebugging(context.threadId) + } + + fun continueDebugging(threadId: Int) + { + coroutineScope.launch { + server.continue_(ContinueArguments().apply { this.threadId = threadId }).await() + } + } + + fun startStepOver(context: PowerShellSuspendContext) + { + startStepOver(context.threadId) + } + + fun startStepOver(threadId: Int) { + coroutineScope.launch { + server.next(NextArguments().apply { this.threadId = threadId }).await() + } + } + + fun startStepInto(context: PowerShellSuspendContext) + { + startStepInto(context.threadId) + } + + fun startStepInto(threadId: Int) { + coroutineScope.launch { + server.stepIn(StepInArguments().apply { this.threadId = threadId }).await() + } + } + fun startStepOut(context: PowerShellSuspendContext) + { + startStepOut(context.threadId) + } + + fun startStepOut(threadId: Int) { + coroutineScope.launch { + server.stepOut(StepOutArguments().apply { this.threadId = threadId }).await() + } + } + + fun startPausing() { + coroutineScope.launch { + server.pause(PauseArguments().apply { threadId = 0 }).await() + } + } + + private val linearizer = Mutex() + private fun sendBreakpointRequest() { + coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { + linearizer.withLock { + for (breakpointMapEntry in breakpointMap) { + val breakpointArgs = SetBreakpointsArguments() + val source: Source = Source() + source.setPath(breakpointMapEntry.key) + breakpointArgs.source = source + + breakpointArgs.breakpoints = breakpointMapEntry.value.map { + val bp = it.value + SourceBreakpoint().apply { + line = bp.line + 1 + condition = bp.conditionExpression?.expression + logMessage = bp.logExpressionObject?.expression + } + }.toTypedArray() + val setBreakpointsResponse = server.setBreakpoints(breakpointArgs).await() + val breakpointsResponse: Array = setBreakpointsResponse.breakpoints + } + } + } + } +} + +class PowerShellExecutionStack(val stackResponse: StackTraceResponse, + val server: IDebugProtocolServer, + val coroutineScope: CoroutineScope, val xDebugSession: XDebugSession): XExecutionStack("PowerShell Debug Execution Stack") { + override fun getTopFrame(): XStackFrame? { + return stackResponse.stackFrames.firstOrNull()?.let { PowerShellStackFrame(it, server, coroutineScope, xDebugSession) } + } + + override fun computeStackFrames(firstFrameIndex: Int, container: XStackFrameContainer?) { + container?.addStackFrames(stackResponse.stackFrames.drop(firstFrameIndex).map { PowerShellStackFrame(it, server, coroutineScope, xDebugSession) }, true) + } + +} + +class PowerShellStackFrame(val stack: StackFrame, val server: IDebugProtocolServer, val coroutineScope: CoroutineScope, val xDebugSession: XDebugSession): XStackFrame() { + + override fun getSourcePosition(): XSourcePosition? { + var file = VfsUtil.findFile(Path(stack.source?.path ?: return null), false) + return XDebuggerUtil.getInstance().createPosition(file, stack.line - 1, stack.column) + } + + override fun getEvaluator(): XDebuggerEvaluator { + return PowershellDebuggerEvaluator(server, this, coroutineScope) + } + + override fun customizePresentation(component: ColoredTextContainer) { + if(stack.source == null) + component.append(stack.name.orEmpty(), SimpleTextAttributes.REGULAR_ATTRIBUTES) + else + super.customizePresentation(component) + } + + override fun computeChildren(node: XCompositeNode) { + coroutineScope.launch { + val scopesResponse = server.scopes(ScopesArguments().apply { frameId = stack.id }).await() + val list = XValueChildrenList() + val localScope = scopesResponse.scopes.first { scope -> scope.name.lowercase() == "local" } + val localVariables = server.variables(VariablesArguments().apply { + variablesReference = localScope.variablesReference + }).await() + localVariables.variables.forEach { list.add(it.name, PowerShellDebuggerVariableValue(it, localScope.variablesReference, server, coroutineScope, xDebugSession)) } + + scopesResponse.scopes.filter { x -> x.name.lowercase() != "local" }.forEach { + val variableRef = it.variablesReference + val groupName = it.name + val variables = server.variables(VariablesArguments().apply { + variablesReference = variableRef + }).await() + val group = PowerShellVariableGroup(groupName, variables, variableRef, server, coroutineScope, xDebugSession) + list.addBottomGroup(group) + } + + node.addChildren(list, true) + } + } +} + +class PowerShellDebuggerVariableValue(val variable: Variable, val parentReference: Int?, + val server: IDebugProtocolServer, + val coroutineScope: CoroutineScope, val xDebugSession: XDebugSession) : XNamedValue(variable.name ?: "") { + + init { + val variablesCache = (xDebugSession.suspendContext as PowerShellSuspendContext).variablesCache + variablesCache.getOrDefault((Pair(parentReference, variable.name)), null)?.let { + variable.value = it.value + variable.type = it.type ?: variable.type + variable.variablesReference = variable.variablesReference + variable.namedVariables = it.namedVariables + variable.indexedVariables = it.indexedVariables + } + } + + override fun computePresentation(node: XValueNode, place: XValuePlace) { + //val kind = variable.presentationHint.kind + var icon: Icon? = IconManager.getInstance().getPlatformIcon(PlatformIcons.Variable) + + node.setPresentation(icon, variable.type, variable.value, variable.variablesReference != 0) + } + + override fun computeChildren(node: XCompositeNode) { + coroutineScope.launch { + if (variable.variablesReference != 0) { + val list = XValueChildrenList() + server.variables(VariablesArguments().apply { variablesReference = variable.variablesReference }) + .await().variables.forEach { list.add(it.name, PowerShellDebuggerVariableValue(it, variable.variablesReference, server, coroutineScope, xDebugSession)) } + node.addChildren(list, true) + } + } + } + + override fun getEvaluationExpression(): String? { + return variable.evaluateName + } + + override fun getModifier(): XValueModifier { + return object : XValueModifier() { + override fun getInitialValueEditorText(): String? { + return variable.value + } + + override fun setValue(expression: XExpression, callback: XModificationCallback) { + if(parentReference !is Int) + return + coroutineScope.launch { + val variablesCache = (xDebugSession.suspendContext as PowerShellSuspendContext).variablesCache + try { + val response = server.setVariable(SetVariableArguments().apply { + variablesReference = parentReference + name = variable.name + value = expression.expression + }).await() + variable.value = response.value + variable.type = response.type ?: variable.type + variable.variablesReference = response.variablesReference ?: variable.variablesReference + variable.namedVariables = response.namedVariables ?: variable.namedVariables + variable.indexedVariables = response.indexedVariables ?: variable.indexedVariables + variablesCache[(Pair(parentReference, variable.name))] = variable + + callback.valueModified() + } catch (e: Exception) { + callback.errorOccurred(e.message ?: e.javaClass.simpleName) + } + } + } + } + } + + override fun computeSourcePosition(navigatable: XNavigatable) { + //navigatable.setSourcePosition(PowerShellSourcePosition()) + } +} + +class PowerShellVariableGroup(val groupName: String, val variable: VariablesResponse, val parentReference: Int, + val server: IDebugProtocolServer, + val coroutineScope: CoroutineScope, + val xDebugSession: XDebugSession): XValueGroup(groupName){ + override fun computeChildren(node: XCompositeNode) { + val list = XValueChildrenList() + variable.variables.forEach { + list.add(it.name, PowerShellDebuggerVariableValue(it, parentReference, server, coroutineScope, xDebugSession)) + } + node.addChildren(list, true) + } +} + +class PowerShellSuspendContext(val stack: StackTraceResponse, val server: IDebugProtocolServer, + val coroutineScope: CoroutineScope, + val threadId: Int = 0, val xDebugSession: XDebugSession):XSuspendContext(){ + + val variablesCache: MutableMap, Variable> = mutableMapOf() + override fun getExecutionStacks(): Array { + return arrayOf(PowerShellExecutionStack(stack, server, coroutineScope, xDebugSession)) + } + + override fun getActiveExecutionStack(): XExecutionStack { + return PowerShellExecutionStack(stack, server, coroutineScope, xDebugSession) + } +} diff --git a/src/main/kotlin/com/intellij/plugin/powershell/ide/debugger/PowerShellDebuggerEditorsProvider.kt b/src/main/kotlin/com/intellij/plugin/powershell/ide/debugger/PowerShellDebuggerEditorsProvider.kt index 141e7521..e2f35ec1 100644 --- a/src/main/kotlin/com/intellij/plugin/powershell/ide/debugger/PowerShellDebuggerEditorsProvider.kt +++ b/src/main/kotlin/com/intellij/plugin/powershell/ide/debugger/PowerShellDebuggerEditorsProvider.kt @@ -1,5 +1,6 @@ package com.intellij.plugin.powershell.ide.debugger +import com.intellij.javascript.debugger.execution.xDebugProcessStarter import com.intellij.openapi.editor.Document import com.intellij.openapi.fileTypes.FileType import com.intellij.openapi.project.Project @@ -7,15 +8,25 @@ import com.intellij.plugin.powershell.PowerShellFileType import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiElement import com.intellij.psi.PsiFileFactory +import com.intellij.testFramework.utils.editor.saveToDisk import com.intellij.util.LocalTimeCounter +import com.intellij.xdebugger.XDebugSession +import com.intellij.xdebugger.XDebuggerManager import com.intellij.xdebugger.XExpression import com.intellij.xdebugger.XSourcePosition import com.intellij.xdebugger.evaluation.EvaluationMode import com.intellij.xdebugger.evaluation.XDebuggerEditorsProvider +import java.util.concurrent.atomic.AtomicInteger -class PowerShellDebuggerEditorsProvider: XDebuggerEditorsProvider() { +val documentSessionKey = com.intellij.openapi.util.Key.create("DocumentSessionKey") + +class PowerShellDebuggerEditorsProvider(val xDebugSession: XDebugSession? = null, val fileName: String = "pwsh"): XDebuggerEditorsProvider() { private var myFileType: PowerShellFileType = PowerShellFileType.INSTANCE + companion object { + private var documentId = AtomicInteger(0); + } + override fun getFileType(): FileType { return myFileType } @@ -26,13 +37,16 @@ class PowerShellDebuggerEditorsProvider: XDebuggerEditorsProvider() { sourcePosition: XSourcePosition?, mode: EvaluationMode ): Document { + val id = documentId.getAndIncrement() val psiFile = PsiFileFactory.getInstance(project) .createFileFromText( - "pwsh." + myFileType.getDefaultExtension(), myFileType, expression.expression, + "$fileName$id." + myFileType.getDefaultExtension(), myFileType, expression.expression, LocalTimeCounter.currentTime(), true ) - val document = checkNotNull(PsiDocumentManager.getInstance(project).getDocument(psiFile)) + //document.saveToDisk() + val session = xDebugSession ?: XDebuggerManager.getInstance(project).currentSession + document.putUserData(documentSessionKey, session) return document } } diff --git a/src/main/kotlin/com/intellij/plugin/powershell/ide/debugger/PowershellDebuggerEvaluator.kt b/src/main/kotlin/com/intellij/plugin/powershell/ide/debugger/PowershellDebuggerEvaluator.kt new file mode 100644 index 00000000..cae1b7d2 --- /dev/null +++ b/src/main/kotlin/com/intellij/plugin/powershell/ide/debugger/PowershellDebuggerEvaluator.kt @@ -0,0 +1,35 @@ +package com.intellij.plugin.powershell.ide.debugger + +import com.intellij.openapi.editor.Document +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.TextRange +import com.intellij.util.io.await +import com.intellij.xdebugger.XSourcePosition +import com.intellij.xdebugger.evaluation.ExpressionInfo +import com.intellij.xdebugger.evaluation.XDebuggerEvaluator +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.eclipse.lsp4j.debug.EvaluateArguments +import org.eclipse.lsp4j.debug.Variable +import org.eclipse.lsp4j.debug.services.IDebugProtocolServer +import org.jetbrains.concurrency.Promise + +class PowershellDebuggerEvaluator(val server: IDebugProtocolServer, val stackFrame: PowerShellStackFrame, val coroutineScope: CoroutineScope): XDebuggerEvaluator() { + override fun evaluate(expression: String, callback: XEvaluationCallback, expressionPosition: XSourcePosition?) { + coroutineScope.launch { + val result = server.evaluate(EvaluateArguments().apply { + this.expression = expression + frameId = stackFrame.stack.id + }).await() + val variable : Variable = Variable().apply { + value = result.result + evaluateName = expression + variablesReference = result.variablesReference + type = result.type + presentationHint = result.presentationHint + } + callback.evaluated(PowerShellDebuggerVariableValue(variable, null, server, coroutineScope, stackFrame.xDebugSession)) + } + } + +} diff --git a/src/main/kotlin/com/intellij/plugin/powershell/ide/editor/completion/PowerShellCompletionContributor.kt b/src/main/kotlin/com/intellij/plugin/powershell/ide/editor/completion/PowerShellCompletionContributor.kt index 3b38eb2b..2f47c12e 100644 --- a/src/main/kotlin/com/intellij/plugin/powershell/ide/editor/completion/PowerShellCompletionContributor.kt +++ b/src/main/kotlin/com/intellij/plugin/powershell/ide/editor/completion/PowerShellCompletionContributor.kt @@ -13,6 +13,7 @@ import com.intellij.patterns.ElementPattern import com.intellij.patterns.PatternCondition import com.intellij.patterns.PlatformPatterns.psiElement import com.intellij.patterns.StandardPatterns.or +import com.intellij.plugin.powershell.ide.debugger.documentSessionKey import com.intellij.plugin.powershell.ide.resolve.* import com.intellij.plugin.powershell.ide.search.PowerShellComponentType import com.intellij.plugin.powershell.lang.lsp.ide.EditorEventManager @@ -55,6 +56,8 @@ class PowerShellCompletionContributor : CompletionContributor() { } override fun fillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) { + if(parameters.editor.document.getUserData(documentSessionKey) != null) + return @Suppress("UnstableApiUsage") val gotResultFromHost = runBlockingCancellable { getCompletionFromLanguageHost(parameters, result) } diff --git a/src/main/kotlin/com/intellij/plugin/powershell/ide/editor/completion/PowerShellDebuggerCompletionContributor.kt b/src/main/kotlin/com/intellij/plugin/powershell/ide/editor/completion/PowerShellDebuggerCompletionContributor.kt new file mode 100644 index 00000000..4866bb35 --- /dev/null +++ b/src/main/kotlin/com/intellij/plugin/powershell/ide/editor/completion/PowerShellDebuggerCompletionContributor.kt @@ -0,0 +1,276 @@ +package com.intellij.plugin.powershell.ide.editor.completion + +import com.intellij.codeInsight.completion.* +import com.intellij.codeInsight.completion.impl.CamelHumpMatcher +import com.intellij.codeInsight.completion.util.ParenthesesInsertHandler +import com.intellij.codeInsight.lookup.AutoCompletionPolicy +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.application.writeAction +import com.intellij.openapi.progress.runBlockingCancellable +import com.intellij.openapi.rd.util.withUiContext +import com.intellij.openapi.util.Key +import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.vfs.findDocument +import com.intellij.patterns.ElementPattern +import com.intellij.patterns.PatternCondition +import com.intellij.patterns.PlatformPatterns.psiElement +import com.intellij.patterns.StandardPatterns.or +import com.intellij.plugin.powershell.ide.debugger.PowerShellDebugProcess +import com.intellij.plugin.powershell.ide.debugger.documentSessionKey +import com.intellij.plugin.powershell.ide.resolve.* +import com.intellij.plugin.powershell.ide.search.PowerShellComponentType +import com.intellij.plugin.powershell.lang.lsp.ide.EditorEventManager +import com.intellij.plugin.powershell.lang.lsp.psi.LSPWrapperPsiElementImpl +import com.intellij.plugin.powershell.lang.lsp.util.DocumentUtils.offsetToLSPPos +import com.intellij.plugin.powershell.psi.* +import com.intellij.plugin.powershell.psi.types.PowerShellClassType +import com.intellij.plugin.powershell.psi.types.PowerShellType +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiNamedElement +import com.intellij.psi.ResolveState +import com.intellij.psi.impl.source.resolve.ResolveCache +import com.intellij.psi.tree.IElementType +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.testFramework.utils.vfs.getDocument +import com.intellij.util.ProcessingContext +import com.intellij.xdebugger.XDebugSession +import kotlinx.coroutines.runBlocking +import org.eclipse.lsp4j.CompletionItem +import org.eclipse.lsp4j.CompletionItemKind +import org.eclipse.lsp4j.TextDocumentIdentifier + +private val IS_BRACED_VARIABLE_CONTEXT: Key = Key.create("IS_BRACED_VARIABLE_CONTEXT") +private val IS_VARIABLE_CONTEXT: Key = Key.create("IS_VARIABLE_CONTEXT") +private val PUT_VARIABLE_CONTEXT: PatternCondition = + object : PatternCondition("In variable") { + override fun accepts(t: PsiElement, context: ProcessingContext): Boolean { + context.put(IS_VARIABLE_CONTEXT, true) + context.put(IS_BRACED_VARIABLE_CONTEXT, t.node.elementType === PowerShellTypes.BRACED_ID) + return true + } + } +private val TYPE_REFERENCE: ElementPattern = + psiElement().withParent(PowerShellReferenceTypeElement::class.java) +private val VARIABLE: ElementPattern = psiElement().withParent(PowerShellIdentifier::class.java) + .withSuperParent(2, PowerShellVariable::class.java).with(PUT_VARIABLE_CONTEXT) + +class PowerShellDebuggerCompletionContributor : CompletionContributor() { + + override fun beforeCompletion(context: CompletionInitializationContext) { + //context.dummyIdentifier = CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED + super.beforeCompletion(context) + } + + override fun fillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) { + val debugSession = parameters.editor.document.getUserData(documentSessionKey) ?: return + val debugProcess = debugSession.debugProcess as PowerShellDebugProcess + + @Suppress("UnstableApiUsage") val gotResultFromHost = runBlockingCancellable { + getCompletionFromLanguageHost(parameters, result, debugSession) + } + if (gotResultFromHost) return + super.fillCompletionVariants(parameters, result) + } + + private suspend fun getCompletionFromLanguageHost(parameters: CompletionParameters, result: CompletionResultSet, xDebugSession: XDebugSession): Boolean { + val position = parameters.position + val offset = parameters.offset + val editor = parameters.editor + val manager = EditorEventManager.forEditor(editor) + if (manager != null) { + val serverPos = offsetToLSPPos(editor, offset) + val toAdd2 = manager.completion(serverPos) + val psiFile = position.containingFile + ?: PsiDocumentManager.getInstance(position.project).getPsiFile(editor.document) + if (toAdd2.items.isNotEmpty()) { + val newResult = adjustPrefixMatcher(result) + for (item in toAdd2.items) { + newResult.addElement(createLookupItem(item, position, psiFile ?: position)) + } + if (cmdNameCondition.accepts(position, null)) { + PowerShellTokenTypeSets.KEYWORDS.types.forEach { keyword -> newResult.addElement(buildKeyword(keyword)) } + } else if (typeBody.accepts(position)) { + addTypeBodyKeywords(newResult) + } + return true + } + } + return false + } + + private fun adjustPrefixMatcher(result: CompletionResultSet): CompletionResultSet { + var prefix = result.prefixMatcher.prefix + val lastDotIndex = prefix.indexOfLast { c -> c == '.' } + if (lastDotIndex > 0 && lastDotIndex + 1 <= prefix.length) { + prefix = prefix.drop(lastDotIndex + 1) + } + return result.withPrefixMatcher(CamelHumpMatcher(prefix, false)) + } + + private val cmdNameCondition = object : PatternCondition("Command Name Condition") { + override fun accepts(element: PsiElement, context: ProcessingContext?): Boolean { + return PsiTreeUtil.getParentOfType(element, PowerShellCommandName::class.java, true, PowerShellBlockBody::class.java) != null + } + } + + private val commandName: ElementPattern = psiElement().withParent(PowerShellIdentifier::class.java) + .withSuperParent(2, PowerShellCommandName::class.java) + private val memberAccess: ElementPattern = psiElement().withParent(PowerShellReferenceIdentifier::class.java) + .withSuperParent(2, or( + psiElement(PowerShellMemberAccessExpression::class.java), + psiElement(PowerShellInvocationExpression::class.java) + ) + ).with(PUT_VARIABLE_CONTEXT) + private val typeBody: ElementPattern = psiElement().withParent(PowerShellBlockBody::class.java) + .withSuperParent(2, PowerShellTypeDeclaration::class.java) + + init { + extend(CompletionType.BASIC, commandName, + object : CompletionProvider() { + override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) { + val ref = PsiTreeUtil.getContextOfType(parameters.position.context, PowerShellReferencePsiElement::class.java) + ?: return + val elements = ResolveCache.getInstance(parameters.originalFile.project) + .resolveWithCaching(ref, PowerShellComponentResolveProcessor.INSTANCE, true, true) ?: emptyList() + + elements.forEach { e -> + result.addElement(buildLookupElement(e, context)) + if (e is PowerShellVariable) { + addFunctionCall(result, e) + } + } + PowerShellTokenTypeSets.KEYWORDS.types.forEach { keyword -> result.addElement(buildKeyword(keyword)) } + } + }) + + extend(CompletionType.BASIC, VARIABLE, + object : CompletionProvider() { + override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) { + val ref = PsiTreeUtil.getContextOfType(parameters.position.context, PowerShellReferencePsiElement::class.java) + ?: return + val elements = ResolveCache.getInstance(parameters.originalFile.project) + .resolveWithCaching(ref, PowerShellComponentResolveProcessor.INSTANCE, true, true) ?: emptyList() + + elements.filterIsInstance().forEach { result.addElement(buildLookupElement(it, context)) } + result.addElement(buildKeyword(PowerShellTypes.THIS)) + } + }) + + extend(CompletionType.BASIC, memberAccess, + object : CompletionProvider() { + override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) { + val expr = PsiTreeUtil.getContextOfType(parameters.position.context, PowerShellMemberAccessExpression::class.java) + ?: return + val qType = expr.qualifier?.getType() + if (qType != null && qType != PowerShellType.UNKNOWN) { + val membersProcessor = PowerShellMemberScopeProcessor() + PowerShellResolveUtil.processMembersForType(qType, membersProcessor) + val res = membersProcessor.getResult() + res.map { it.element }.filter { it !is PowerShellConstructorDeclarationStatement }.forEach { result.addElement(buildLookupElement(it, context)) } + if (expr.isTypeMemberAccess()) { + addCallNew(result, qType) + } + } + } + }) + + extend(CompletionType.BASIC, TYPE_REFERENCE, + object : CompletionProvider() { + override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) { + val ref = PsiTreeUtil.getContextOfType(parameters.position, PowerShellReferenceTypeElement::class.java) + ?: return + val resolveProcessor = PowerShellClassScopeProcessor() + PsiTreeUtil.treeWalkUp(resolveProcessor, ref, null, ResolveState.initial()) + resolveProcessor.getResult().map { it.element }.forEach { result.addElement(buildLookupElement(it, context)) } + } + }) + + extend(CompletionType.BASIC, typeBody, + object : CompletionProvider() { + override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) { + addTypeBodyKeywords(result) + } + }) + } + + private fun addTypeBodyKeywords(result: CompletionResultSet) { + result.addElement(buildKeyword("$${PowerShellTypes.THIS}")) + result.addElement(buildKeyword(PowerShellTypes.STATIC)) + result.addElement(buildKeyword(PowerShellTypes.HIDDEN)) + } + + private fun addFunctionCall(result: CompletionResultSet, variable: PowerShellVariable) { + if (PsNames.NAMESPACE_FUNCTION.equals(variable.getScopeName(), true)) { + result.addElement(buildCall(variable.name ?: variable.text)) + } + } + + private fun buildCall(name: String) = LookupElementBuilder.create(name).withIcon(PowerShellComponentType.FUNCTION.getIcon()) + + private fun addCallNew(result: CompletionResultSet, qualifierType: PowerShellType) { + if (qualifierType !is PowerShellClassType) return + val resolved = qualifierType.resolve() as? PowerShellClassDeclaration ?: return + var e = LookupElementBuilder.create("new").withIcon(PowerShellComponentType.METHOD.getIcon()) + e = if (PowerShellResolveUtil.hasDefaultConstructor(resolved)) { + e.withInsertHandler(ParenthesesInsertHandler.NO_PARAMETERS) + } else { + e.withInsertHandler(ParenthesesInsertHandler.WITH_PARAMETERS) + } + result.addElement(e) + } + + private fun buildKeyword(kw: IElementType): LookupElement { + return buildKeyword(kw.toString()) + } + + private fun buildKeyword(kw: String): LookupElement { + return LookupElementBuilder.create(kw.lowercase()).bold() + } + + private fun buildLookupElement(e: PsiElement, context: ProcessingContext): LookupElement { + val icon = e.getIcon(0) + return when (e) { + is PowerShellVariable -> { + val lookupString = getVariableLookupString(context, e) + LookupElementBuilder.create(e, lookupString).withIcon(icon).withPresentableText(lookupString) + } + is PowerShellCallableDeclaration -> LookupElementBuilder.create(e).withIcon(icon).withInsertHandler(ParenthesesInsertHandler.WITH_PARAMETERS) + is PowerShellComponent -> LookupElementBuilder.create(e).withIcon(icon) + else -> LookupElementBuilder.create(e) + } + } + + private fun getVariableLookupString(context: ProcessingContext, e: PowerShellVariable): String { + return if (context.get(IS_VARIABLE_CONTEXT) == true) { + if (e.isBracedVariable() && context.get(IS_BRACED_VARIABLE_CONTEXT) == false) { + (e.presentation.presentableText ?: e.text).trimStart('$') + } else + e.name ?: e.text + } else { + e.presentation.presentableText ?: e.text + } + } + + + private fun createLookupItem(item: CompletionItem, position: PsiElement, parent: PsiElement): LookupElement { + val insertText = item.insertText + val kind = item.kind + val label = item.label + val presentableText = if (StringUtil.isNotEmpty(label)) label else insertText ?: "" + val tailText = item.detail ?: "" + val isCompletingType = TYPE_REFERENCE.accepts(position) + val lspElement = LSPWrapperPsiElementImpl(item, parent) + if (isCompletingType) { + lspElement.setType(PowerShellComponentType.CLASS) + } + var builder: LookupElementBuilder = LookupElementBuilder.create(lspElement as PsiNamedElement).withIcon(lspElement.presentation.getIcon(false)) + if (lspElement.getType() == PowerShellComponentType.METHOD) { + builder = builder.withInsertHandler(ParenthesesInsertHandler.WITH_PARAMETERS) + } + if (kind == CompletionItemKind.Keyword) builder = builder.withBoldness(true) + return builder.withPresentableText(presentableText).appendTailText(" $tailText", true).withAutoCompletionPolicy(AutoCompletionPolicy.SETTINGS_DEPENDENT) + } +} diff --git a/src/main/kotlin/com/intellij/plugin/powershell/ide/run/PowerShellProgramDebugRunner.kt b/src/main/kotlin/com/intellij/plugin/powershell/ide/run/PowerShellProgramDebugRunner.kt index 5db80769..5df72df2 100644 --- a/src/main/kotlin/com/intellij/plugin/powershell/ide/run/PowerShellProgramDebugRunner.kt +++ b/src/main/kotlin/com/intellij/plugin/powershell/ide/run/PowerShellProgramDebugRunner.kt @@ -8,12 +8,14 @@ import com.intellij.execution.configurations.RunnerSettings import com.intellij.execution.executors.DefaultDebugExecutor import com.intellij.execution.runners.AsyncProgramRunner import com.intellij.execution.runners.ExecutionEnvironment +import com.intellij.execution.runners.showRunContent import com.intellij.execution.ui.RunContentDescriptor import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.rd.util.toPromise import com.intellij.openapi.rd.util.withUiContext import com.intellij.plugin.powershell.ide.PluginProjectRoot import com.intellij.plugin.powershell.ide.debugger.PowerShellDebugProcess +import com.intellij.plugin.powershell.ide.debugger.PowerShellDebugSession import com.intellij.xdebugger.XDebugProcess import com.intellij.xdebugger.XDebugProcessStarter import com.intellij.xdebugger.XDebugSession @@ -46,15 +48,22 @@ class PowerShellProgramDebugRunner : AsyncProgramRunner() { FileDocumentManager.getInstance().saveAllDocuments() } state.prepareExecution() - val executionResult = state.execute(environment.executor, this@PowerShellProgramDebugRunner) val descriptor = withUiContext { - XDebuggerManager.getInstance(environment.project).startSession(environment, object : XDebugProcessStarter() { + val debuggerManager = XDebuggerManager.getInstance(environment.project) + debuggerManager.startSession(environment, object : XDebugProcessStarter() { @Throws(ExecutionException::class) override fun start(session: XDebugSession): XDebugProcess { - return PowerShellDebugProcess(session, executionResult) + environment.putUserData(XSessionKey, session) + val executionResult = state.execute(environment.executor, this@PowerShellProgramDebugRunner) + val clientSession = environment.getUserData(ClientSessionKey) + return PowerShellDebugProcess(session, executionResult!!, debuggerManager, clientSession!!) //todo add null check } }).runContentDescriptor } descriptor + //withUiContext { showRunContent(executionResult, environment) } }.toPromise() } + +val XSessionKey = com.intellij.openapi.util.Key.create("PowerShellXDebugSession") +val ClientSessionKey = com.intellij.openapi.util.Key.create("PowerShellDebugSession") diff --git a/src/main/kotlin/com/intellij/plugin/powershell/ide/run/PowerShellScriptCommandLineState.kt b/src/main/kotlin/com/intellij/plugin/powershell/ide/run/PowerShellScriptCommandLineState.kt index 93668caf..b065760a 100644 --- a/src/main/kotlin/com/intellij/plugin/powershell/ide/run/PowerShellScriptCommandLineState.kt +++ b/src/main/kotlin/com/intellij/plugin/powershell/ide/run/PowerShellScriptCommandLineState.kt @@ -4,8 +4,10 @@ import com.intellij.execution.DefaultExecutionResult import com.intellij.execution.ExecutionException import com.intellij.execution.ExecutionResult import com.intellij.execution.Executor -import com.intellij.execution.configurations.PtyCommandLine +import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.configurations.RunProfileState +import com.intellij.execution.executors.DefaultDebugExecutor +import com.intellij.execution.executors.DefaultRunExecutor import com.intellij.execution.process.KillableProcessHandler import com.intellij.execution.process.ProcessHandler import com.intellij.execution.runners.ExecutionEnvironment @@ -15,22 +17,41 @@ import com.intellij.openapi.application.readAction import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.diagnostic.runAndLogException import com.intellij.openapi.options.advanced.AdvancedSettings +import com.intellij.openapi.project.Project import com.intellij.openapi.roots.ProjectRootManager import com.intellij.openapi.util.io.NioFiles.toPath import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.plugin.powershell.ide.PluginProjectRoot +import com.intellij.plugin.powershell.ide.debugger.PowerShellBreakpointType +import com.intellij.plugin.powershell.ide.debugger.PowerShellDebugSession +import com.intellij.plugin.powershell.lang.debugger.PSDebugClient import com.intellij.plugin.powershell.lang.lsp.LSPInitMain +import com.intellij.plugin.powershell.lang.lsp.languagehost.EditorServicesLanguageHostStarter import com.intellij.plugin.powershell.lang.lsp.languagehost.PowerShellNotInstalled import com.intellij.terminal.TerminalExecutionConsole +import com.intellij.util.io.await import com.intellij.util.text.nullize +import com.intellij.xdebugger.XDebugSession +import com.intellij.xdebugger.XDebuggerManager import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import org.eclipse.lsp4j.debug.* +import org.eclipse.lsp4j.debug.launch.DSPLauncher +import org.eclipse.lsp4j.debug.services.IDebugProtocolServer +import org.eclipse.lsp4j.jsonrpc.Launcher import org.jetbrains.annotations.TestOnly import java.io.File +import java.io.InputStream +import java.io.OutputStream import java.nio.charset.Charset import java.nio.file.Path +import java.util.concurrent.TimeUnit import java.util.regex.Pattern + class PowerShellScriptCommandLineState( private val runConfiguration: PowerShellRunConfiguration, private val environment: ExecutionEnvironment @@ -59,8 +80,8 @@ class PowerShellScriptCommandLineState( runConfiguration.getCommandOptions(), runConfiguration.scriptParameters ) - val commandLine = PtyCommandLine(command) - .withConsoleMode(false) + val commandLine = GeneralCommandLine(command) + //.withConsoleMode(false) .withWorkDirectory(workingDirectory.toString()) .withCharset(getTerminalCharSet()) @@ -105,13 +126,98 @@ class PowerShellScriptCommandLineState( return commandString } - override fun execute(executor: Executor?, runner: ProgramRunner<*>): ExecutionResult { - val process = startProcess() - val console = TerminalExecutionConsole(environment.project, process) - return DefaultExecutionResult(console, process) + override fun execute(executor: Executor?, runner: ProgramRunner<*>): ExecutionResult? { + if (executor?.id == DefaultRunExecutor.EXECUTOR_ID) { + val process = startProcess() + val console = TerminalExecutionConsole(environment.project, process) + return DefaultExecutionResult(console, process) + } else if (executor?.id == DefaultDebugExecutor.EXECUTOR_ID) { + val processRunner = EditorServicesLanguageHostStarter(environment.project) + processRunner.useConsoleRepl() + return runBlocking { + val InOutPair = processRunner.establishDebuggerConnection() + val process = processRunner.getProcess() ?: return@runBlocking null + val handler = KillableProcessHandler(process, "PowerShellEditorService") + val console = TerminalExecutionConsole(environment.project, handler) + handler.startNotify() + val session = environment.getUserData(XSessionKey) + processDebuging(InOutPair.first!!, InOutPair.second!!, session!!) //todo nullcheck + DefaultExecutionResult(console, handler) + } + } else { + error("Unknown executor") + } + } + private fun processDebuging(inputStream: InputStream, outputStream: OutputStream, debugSession: XDebugSession){ + val targetPath = runConfiguration.scriptPath + + val client = PSDebugClient(debugSession) + val launcher: Launcher = DSPLauncher.createClientLauncher(client, inputStream, outputStream) + launcher.startListening() + + val arguments = InitializeRequestArguments() + arguments.clientID = "client1" + arguments.adapterID = "adapter1" + arguments.setSupportsRunInTerminalRequest(false) + + val remoteProxy = launcher.remoteProxy + + val capabilities: Capabilities = remoteProxy.initialize(arguments)[10, TimeUnit.SECONDS] + + val scope = PluginProjectRoot.getInstance(environment.project).coroutineScope + + val powerShellDebugSession = PowerShellDebugSession(client, remoteProxy, debugSession, scope, debugSession) + environment.putUserData(ClientSessionKey, powerShellDebugSession) + val allBreakpoints = XDebuggerManager.getInstance(environment.project).breakpointManager.getBreakpoints(PowerShellBreakpointType::class.java) + allBreakpoints.filter{x -> x.sourcePosition != null && x.sourcePosition!!.file.exists() && x.sourcePosition!!.file.isValid} + .groupBy { x -> VfsUtil.virtualToIoFile(x.sourcePosition!!.file).toURI().toASCIIString() } + .forEach { entry -> + + val fileURL = entry.key + + val breakpointArgs = SetBreakpointsArguments() + val source: Source = Source() + source.setPath(fileURL) + breakpointArgs.source = source + val bps = entry.value + breakpointArgs.breakpoints = bps.map { + val bp = it + SourceBreakpoint().apply { + line = bp.line + 1 + condition = bp.conditionExpression?.expression + logMessage = bp.logExpressionObject?.expression + } + }.toTypedArray() + val setBreakpointsResponse = remoteProxy.setBreakpoints(breakpointArgs).join() + val breakpointsResponse: Array = setBreakpointsResponse.breakpoints + } + // Add a breakpoint. + /*val breakpointArgs = SetBreakpointsArguments() + val source: Source = Source() + source.setPath(targetPath) + breakpointArgs.source = source + val sourceBreakpoint = SourceBreakpoint() + sourceBreakpoint.line = 1 + val breakpoints = arrayOf(sourceBreakpoint) + breakpointArgs.breakpoints = breakpoints + val future = remoteProxy.setBreakpoints(breakpointArgs) + val setBreakpointsResponse = future[10, TimeUnit.SECONDS] + val breakpointsResponse: Array = setBreakpointsResponse.breakpoints*/ + + val launchArgs: MutableMap = HashMap() + launchArgs["terminal"] = "none" + launchArgs["script"] = targetPath + launchArgs["noDebug"] = false + launchArgs["__sessionId"] = "sessionId" + val launch = remoteProxy.launch(launchArgs).join() + +// Signal that the configuration is finished + remoteProxy.configurationDone(ConfigurationDoneArguments()) } } + + private fun getTerminalCharSet(): Charset { val name = AdvancedSettings.getString("terminal.character.encoding") return logger.runAndLogException { charset(name) } ?: Charsets.UTF_8 diff --git a/src/main/kotlin/com/intellij/plugin/powershell/lang/debugger/PSDebugClient.kt b/src/main/kotlin/com/intellij/plugin/powershell/lang/debugger/PSDebugClient.kt new file mode 100644 index 00000000..261fdca2 --- /dev/null +++ b/src/main/kotlin/com/intellij/plugin/powershell/lang/debugger/PSDebugClient.kt @@ -0,0 +1,89 @@ +package com.intellij.plugin.powershell.lang.debugger + +import com.intellij.xdebugger.XDebugSession +import com.intellij.xdebugger.frame.XSuspendContext +import com.jetbrains.rd.util.reactive.Signal +import org.eclipse.lsp4j.debug.* +import org.eclipse.lsp4j.debug.services.IDebugProtocolClient +import java.util.concurrent.CompletableFuture + +class PSDebugClient(session: XDebugSession): IDebugProtocolClient { + private val debugSession = session + + val debugStopped = Signal() + + override fun breakpoint(args: BreakpointEventArguments?) { + super.breakpoint(args) + /*val bp = debugSession.debugProcess. + debugSession.breakpointReached()*/ + } + + override fun stopped(args: StoppedEventArguments?) { + super.stopped(args) + debugStopped.fire(args) + } + + override fun continued(args: ContinuedEventArguments?) { + super.continued(args) + } + + override fun exited(args: ExitedEventArguments?) { + super.exited(args) + } + + override fun terminated(args: TerminatedEventArguments?) { + super.terminated(args) + } + + override fun thread(args: ThreadEventArguments?) { + super.thread(args) + } + + override fun output(args: OutputEventArguments?) { + super.output(args) + } + + override fun module(args: ModuleEventArguments?) { + super.module(args) + } + + override fun loadedSource(args: LoadedSourceEventArguments?) { + super.loadedSource(args) + } + + override fun process(args: ProcessEventArguments?) { + super.process(args) + } + + override fun capabilities(args: CapabilitiesEventArguments?) { + super.capabilities(args) + } + + override fun progressStart(args: ProgressStartEventArguments?) { + super.progressStart(args) + } + + override fun progressUpdate(args: ProgressUpdateEventArguments?) { + super.progressUpdate(args) + } + + override fun progressEnd(args: ProgressEndEventArguments?) { + super.progressEnd(args) + } + + override fun invalidated(args: InvalidatedEventArguments?) { + super.invalidated(args) + } + + override fun memory(args: MemoryEventArguments?) { + super.memory(args) + } + + override fun runInTerminal(args: RunInTerminalRequestArguments?): CompletableFuture { + return super.runInTerminal(args) + } + + override fun startDebugging(args: StartDebuggingRequestArguments?): CompletableFuture { + return super.startDebugging(args) + } +} diff --git a/src/main/kotlin/com/intellij/plugin/powershell/lang/lsp/languagehost/EditorServicesLanguageHostStarter.kt b/src/main/kotlin/com/intellij/plugin/powershell/lang/lsp/languagehost/EditorServicesLanguageHostStarter.kt index 76176e0f..fe8f1374 100644 --- a/src/main/kotlin/com/intellij/plugin/powershell/lang/lsp/languagehost/EditorServicesLanguageHostStarter.kt +++ b/src/main/kotlin/com/intellij/plugin/powershell/lang/lsp/languagehost/EditorServicesLanguageHostStarter.kt @@ -4,6 +4,7 @@ import com.google.common.io.Files import com.google.gson.JsonObject import com.google.gson.JsonParser import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.configurations.PtyCommandLine import com.intellij.execution.process.* import com.intellij.notification.BrowseNotificationAction import com.intellij.notification.Notification @@ -180,6 +181,57 @@ open class EditorServicesLanguageHostStarter(protected val myProject: Project) : } } + override suspend fun establishDebuggerConnection(): Pair { + try { + val sessionInfo = startServerSession(true) ?: return Pair(null, null) + if (sessionInfo is SessionInfo.Pipes) { + val readPipeName = sessionInfo.debugServiceReadPipeName + val writePipeName = sessionInfo.debugServiceWritePipeName + return withSyncIOBackgroundContext { + if (SystemInfo.isWindows) { + val readPipe = RandomAccessFile(readPipeName, "rwd") + val writePipe = RandomAccessFile(writePipeName, "r") + val serverReadChannel = readPipe.channel + val serverWriteChannel = writePipe.channel + val inSf = Channels.newInputStream(serverWriteChannel) + val outSf = BufferedOutputStream(Channels.newOutputStream(serverReadChannel)) + Pair(inSf, outSf) + } else { + val readSock = AFUNIXSocket.newInstance() + val writeSock = AFUNIXSocket.newInstance() + readSock.connect(AFUNIXSocketAddress.of(File(readPipeName))) + writeSock.connect(AFUNIXSocketAddress.of(File(writePipeName))) + Pair(writeSock.inputStream, readSock.outputStream) + } + } + } else { + return withSyncIOBackgroundContext block@{ + val port = (sessionInfo as? SessionInfo.Tcp)?.debugServicePort ?: return@block Pair(null, null) + try { + socket = Socket("127.0.0.1", port) + } catch (e: Exception) { + logger.error("Unable to open connection to language host: $e") + } + if (socket == null) { + logger.error("Unable to create socket: " + toString()) + } + if (socket?.isConnected == true) { + logger.info("Connection to language host established: ${socket?.localPort} -> ${socket?.port}") + val inputStream = socket?.getInputStream() + val outputStream = socket?.getOutputStream() + if (inputStream != null && outputStream != null) return@block Pair(inputStream, outputStream) + } + + Pair(null, null) + } + } + } + catch (t: Throwable) + { + return Pair(null, null) + } + } + private fun getSessionCount(): Int { return sessionCount.getAndIncrement() } @@ -194,12 +246,13 @@ open class EditorServicesLanguageHostStarter(protected val myProject: Project) : } override fun createProcess(project: Project, command: List, environment: Map?): Process { - return GeneralCommandLine(command) + return PtyCommandLine(command) + .withConsoleMode(false) .withEnvironment(environment) .createProcess() } - private suspend fun buildCommandLine(): List { + private suspend fun buildCommandLine(isDebugServiceOnly: Boolean = false): List { val psExtensionPath = getPowerShellEditorServicesHome() val startupScript = getStartupScriptPath(psExtensionPath) if (StringUtil.isEmpty(startupScript)) { @@ -223,12 +276,14 @@ open class EditorServicesLanguageHostStarter(protected val myProject: Project) : psesVersionString = "-EditorServicesVersion '$psesVersionString'" } val bundledModulesPath = getPSExtensionModulesDir(psExtensionPath) - val useReplSwitch = if (useConsoleRepl()) "-EnableConsoleRepl" else "" - val logLevel = if (useConsoleRepl()) "Normal" else "Diagnostic" - val args = "$psesVersionString -HostName '${myHostDetails.name}' -HostProfileId '${myHostDetails.profileId}' " + + val useReplSwitch = if (useConsoleRepl() || isDebugServiceOnly) "-EnableConsoleRepl" else "" + val logLevel = if (useConsoleRepl() || isDebugServiceOnly) "Normal" else "Diagnostic" + val debugServiceOnlySwitch = if(isDebugServiceOnly) " -DebugServiceOnly" else "" + var args = "$psesVersionString -HostName '${myHostDetails.name}' -HostProfileId '${myHostDetails.profileId}' " + "-HostVersion '${myHostDetails.version}' -AdditionalModules @() " + - "-BundledModulesPath '$bundledModulesPath' $useReplSwitch " + + "-BundledModulesPath '$bundledModulesPath' $useReplSwitch $debugServiceOnlySwitch " + "-LogLevel '$logLevel' -LogPath '$logPath' -SessionDetailsPath '$sessionDetailsPath' -FeatureFlags @() $splitInOutPipesSwitch" + val preamble = if (SystemInfo.isWindows) { when (psVersion.edition) { @@ -271,10 +326,10 @@ open class EditorServicesLanguageHostStarter(protected val myProject: Project) : * @throws PowerShellExtensionNotFound * @throws PowerShellNotInstalled */ - private suspend fun startServerSession(): SessionInfo? { + private suspend fun startServerSession(isDebugServiceOnly: Boolean = false): SessionInfo? { cachedPowerShellExtensionDir = null cachedEditorServicesModuleVersion = null - val commandLine = buildCommandLine() + val commandLine = buildCommandLine(isDebugServiceOnly) val process = createProcess(myProject, commandLine, mapOf(INTELLIJ_POWERSHELL_PARENT_PID to ProcessHandle.current().pid().toString())) val pid: Long = getProcessID(process) processOutput( @@ -366,11 +421,9 @@ open class EditorServicesLanguageHostStarter(protected val myProject: Project) : val debugServiceWritePipeName = jsonResult.get("debugServiceWritePipeName")?.asString val powerShellVersion = jsonResult.get("powerShellVersion")?.asString val status = jsonResult.get("status")?.asString - if (langServiceReadPipeName == null || langServiceWritePipeName == null || debugServiceReadPipeName == null || debugServiceWritePipeName == null) { - logger.warn("languageServiceReadPipeName or debugServiceReadPipeName are null") - return null - } - return SessionInfo.Pipes(langServiceReadPipeName, langServiceWritePipeName, debugServiceReadPipeName, debugServiceWritePipeName, powerShellVersion, status) + + //todo make types nullable + return SessionInfo.Pipes(langServiceReadPipeName ?: "", langServiceWritePipeName ?: "", debugServiceReadPipeName ?: "", debugServiceWritePipeName ?: "", powerShellVersion, status) } private fun readSessionFile(sessionFile: File): SessionInfo? { diff --git a/src/main/kotlin/com/intellij/plugin/powershell/lang/lsp/languagehost/LanguageHostConnectionManager.kt b/src/main/kotlin/com/intellij/plugin/powershell/lang/lsp/languagehost/LanguageHostConnectionManager.kt index 42b8c3a1..5b3629a9 100644 --- a/src/main/kotlin/com/intellij/plugin/powershell/lang/lsp/languagehost/LanguageHostConnectionManager.kt +++ b/src/main/kotlin/com/intellij/plugin/powershell/lang/lsp/languagehost/LanguageHostConnectionManager.kt @@ -6,6 +6,7 @@ import java.io.OutputStream interface LanguageHostConnectionManager { suspend fun establishConnection(): Pair + suspend fun establishDebuggerConnection(): Pair fun closeConnection() fun isConnected(): Boolean fun getProcess(): Process? diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index ae6fbcc7..00c7ffd3 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -92,6 +92,9 @@ + + @@ -120,10 +123,5 @@ - - - diff --git a/src/test/kotlin/com/intellij/plugin/powershell/PowerShellDebuggerTestUtil.kt b/src/test/kotlin/com/intellij/plugin/powershell/PowerShellDebuggerTestUtil.kt new file mode 100644 index 00000000..b8067f33 --- /dev/null +++ b/src/test/kotlin/com/intellij/plugin/powershell/PowerShellDebuggerTestUtil.kt @@ -0,0 +1,21 @@ +package com.intellij.plugin.powershell + +import com.intellij.plugin.powershell.ide.debugger.PowerShellSuspendContext +import com.intellij.xdebugger.XDebuggerTestUtil +import com.intellij.xdebugger.XTestCompositeNode +import com.intellij.xdebugger.frame.XValue + +class PowerShellDebuggerTestUtil { + companion object { + fun getVariable(suspendContext: PowerShellSuspendContext, variableName: String): XValue { + val topFrame = suspendContext.activeExecutionStack.topFrame!! + val children = XTestCompositeNode(topFrame).collectChildren() + return XDebuggerTestUtil.findVar(children, variableName) + } + + fun getChildren(suspendContext: PowerShellSuspendContext, variableName: String): MutableList { + val topFrame = suspendContext.activeExecutionStack.topFrame!! + return XTestCompositeNode(topFrame).collectChildren() + } + } +} diff --git a/src/test/kotlin/com/intellij/plugin/powershell/debugger/BreakpointTest.kt b/src/test/kotlin/com/intellij/plugin/powershell/debugger/BreakpointTest.kt new file mode 100644 index 00000000..ddf710b3 --- /dev/null +++ b/src/test/kotlin/com/intellij/plugin/powershell/debugger/BreakpointTest.kt @@ -0,0 +1,71 @@ +package com.intellij.plugin.powershell.debugger + +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.plugin.powershell.ide.debugger.PowerShellDebugProcess +import com.intellij.plugin.powershell.ide.debugger.PowerShellSuspendContext +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.testFramework.fixtures.TempDirTestFixture +import com.intellij.testFramework.fixtures.impl.TempDirTestFixtureImpl +import com.intellij.xdebugger.XDebuggerTestUtil +import com.intellij.xdebugger.XTestCompositeNode +import com.intellij.xdebugger.XTestValueNode +import com.intellij.xdebugger.frame.XNamedValue +import com.intellij.xdebugger.frame.XValue +import com.intellij.xdebugger.frame.XValuePlace +import junit.framework.TestCase +import java.io.File + +class BreakpointTest: BasePlatformTestCase() { + + override fun getTestDataPath() = "src/test/resources/testData" + + override fun createTempDirTestFixture(): TempDirTestFixture { + return TempDirTestFixtureImpl() + } + + fun testBreakpoint() { + val psiFile = myFixture.configureByFile("debugger/testBreakpoint.ps1") + val file = psiFile.virtualFile + + val fileLine = 5 // line in file, starting from 1 + val line = fileLine - 1 // breakpoint line, starting from 0 + val testSession = PowerShellTestSession(project, file.toNioPath()) + XDebuggerTestUtil.toggleBreakpoint(project, file, line) + val debugSession = testSession.startDebugSession() + XDebuggerTestUtil.waitFor(testSession.sessionListener.pausedSemaphore, testSession.waitForBackgroundTimeout.toMillis()) + val suspendContext = debugSession.suspendContext as PowerShellSuspendContext + TestCase.assertEquals(line, suspendContext.activeExecutionStack.topFrame?.sourcePosition?.line) + myFixture.projectDisposable.dispose() + } + + fun testConditionalBreakpoint() + { + val psiFile = myFixture.configureByFile("debugger/testBreakpoint.ps1") + val file = psiFile.virtualFile + + val fileLine = 5 // line in file, starting from 1 + val line = fileLine - 1 // breakpoint line, starting from 0 + val variableName = "\$val" + val value = 2 + val condition = "$variableName -eq $value" + + val testSession = PowerShellTestSession(project, file.toNioPath()) + XDebuggerTestUtil.toggleBreakpoint(project, file, line) + XDebuggerTestUtil.setBreakpointCondition(project, line, condition) + val debugSession = testSession.startDebugSession() + XDebuggerTestUtil.waitFor(testSession.sessionListener.pausedSemaphore, testSession.waitForBackgroundTimeout.toMillis()) + val suspendContext = debugSession.suspendContext as PowerShellSuspendContext + val topFrame = suspendContext.activeExecutionStack.topFrame!! + val children = XTestCompositeNode(topFrame).collectChildren() + val variableValue = XDebuggerTestUtil.findVar(children, variableName) + val variableValueNode = XDebuggerTestUtil.computePresentation(variableValue) + TestCase.assertEquals(value.toString(), variableValueNode.myValue) + myFixture.projectDisposable.dispose() + } + + override fun tearDown(){ + myFixture.projectDisposable.dispose() + myFixture.tearDown() + } + +} diff --git a/src/test/kotlin/com/intellij/plugin/powershell/debugger/EvaluationTest.kt b/src/test/kotlin/com/intellij/plugin/powershell/debugger/EvaluationTest.kt new file mode 100644 index 00000000..e77afdfb --- /dev/null +++ b/src/test/kotlin/com/intellij/plugin/powershell/debugger/EvaluationTest.kt @@ -0,0 +1,36 @@ +package com.intellij.plugin.powershell.debugger + +import com.intellij.plugin.powershell.ide.debugger.PowerShellSuspendContext +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.testFramework.fixtures.TempDirTestFixture +import com.intellij.testFramework.fixtures.impl.TempDirTestFixtureImpl +import com.intellij.vcs.commit.CommitSessionInfo.Default.session +import com.intellij.xdebugger.XDebuggerTestUtil +import junit.framework.TestCase + +class EvaluationTest: BasePlatformTestCase() { + override fun getTestDataPath() = "src/test/resources/testData" + + override fun createTempDirTestFixture(): TempDirTestFixture { + return TempDirTestFixtureImpl() + } + + fun testEvaluation() { + val psiFile = myFixture.configureByFile("debugger/testBreakpoint.ps1") + val file = psiFile.virtualFile + + val fileLine = 1 // line in file, starting from 1 + val line = fileLine - 1 // breakpoint line, starting from 0 + val expression = "1 + 2" + val expectedResult = "3" + + val testSession = PowerShellTestSession(project, file.toNioPath()) + XDebuggerTestUtil.toggleBreakpoint(project, file, line) + val debugSession = testSession.startDebugSession() + XDebuggerTestUtil.waitFor(testSession.sessionListener.pausedSemaphore, testSession.waitForBackgroundTimeout.toMillis()) + val variableValue = XDebuggerTestUtil.evaluate(debugSession, expression, testSession.waitForBackgroundTimeout.toMillis()).first + val variableValueNode = XDebuggerTestUtil.computePresentation(variableValue) + TestCase.assertEquals(expectedResult, variableValueNode.myValue) + myFixture.projectDisposable.dispose() + } +} diff --git a/src/test/kotlin/com/intellij/plugin/powershell/debugger/PowerShellTestSession.kt b/src/test/kotlin/com/intellij/plugin/powershell/debugger/PowerShellTestSession.kt new file mode 100644 index 00000000..5044ba88 --- /dev/null +++ b/src/test/kotlin/com/intellij/plugin/powershell/debugger/PowerShellTestSession.kt @@ -0,0 +1,126 @@ +package com.intellij.plugin.powershell.debugger + +import com.intellij.execution.ExecutionException +import com.intellij.execution.executors.DefaultDebugExecutor +import com.intellij.execution.impl.RunManagerImpl +import com.intellij.execution.impl.RunnerAndConfigurationSettingsImpl +import com.intellij.execution.runners.ExecutionEnvironment +import com.intellij.execution.runners.ProgramRunner +import com.intellij.openapi.project.Project +import com.intellij.plugin.powershell.ide.debugger.PowerShellDebugProcess +import com.intellij.plugin.powershell.ide.run.* +import com.intellij.xdebugger.* +import kotlinx.coroutines.runBlocking +import java.nio.file.Path +import java.time.Duration +import java.util.concurrent.Semaphore +import kotlin.io.path.Path +import com.jetbrains.rd.util.reactive.Signal +import kotlin.io.path.pathString + +class PowerShellTestSession(val project: Project, val scriptPath: Path) { + val waitForStopTimeout: Duration = Duration.ofMinutes(3) + val waitForBackgroundTimeout: Duration = Duration.ofSeconds(10) + val sessionListener: PowerShellDebugSessionListener = PowerShellDebugSessionListener() + + private val projectPath: Path + get() = Path(project.basePath!!) + private val defaultWorkingDirectory + get() = projectPath.resolve("scripts") + + val configuration: PowerShellRunConfiguration = createConfiguration(defaultWorkingDirectory.toString(), scriptPath.pathString) + + fun startDebugSession() = startDebugSession(configuration) + + fun startDebugSession(configuration: PowerShellRunConfiguration): XDebugSession { + val executor = DefaultDebugExecutor.getDebugExecutorInstance() + val runner = ProgramRunner.getRunner(executor.id, configuration) as PowerShellProgramDebugRunner + val environment = ExecutionEnvironment( + executor, + runner, + RunnerAndConfigurationSettingsImpl(RunManagerImpl.getInstanceImpl(project), configuration), + project + ) + val state = configuration.getState(executor, environment) + runBlocking { state.prepareExecution() } + val debuggerManager = XDebuggerManager.getInstance(environment.project) + + val session = debuggerManager.startSession(environment, object : XDebugProcessStarter() { + @Throws(ExecutionException::class) + override fun start(session: XDebugSession): XDebugProcess { + environment.putUserData(XSessionKey, session) + val executionResult = state.execute(environment.executor, runner) + val clientSession = environment.getUserData(ClientSessionKey) + return PowerShellDebugProcess( + session, + executionResult!!, + debuggerManager, + clientSession!! + ) + } + }) + session.addSessionListener(sessionListener) + return session + } + + fun createConfiguration( + customWorkingDirectory: String?, + customScriptPath: String? + ): PowerShellRunConfiguration { + val type = PowerShellConfigurationType() + val factory = type.configurationFactories.single() + return PowerShellRunConfiguration(project, factory, "Test").apply { + scriptPath = customScriptPath ?: projectPath.resolve("scripts/MyScript.ps1").toString() + this.customWorkingDirectory = customWorkingDirectory + } + } +} + +class PowerShellDebugSessionListener : XDebugSessionListener { + + // if greater than 0 -> paused, resumed, stopped + val pausedSemaphore = Semaphore(0) + val resumedSemaphore = Semaphore(0) + val stoppedSemaphore = Semaphore(0) + + val pausedEvent = Signal() + val resumedEvent = Signal() + val stoppedEvent = Signal() + + override fun sessionPaused() { + pausedEvent.fire(Unit) + if (pausedSemaphore.availablePermits() > 0) + return + pausedSemaphore.release() + } + + override fun sessionResumed() { + resumedEvent.fire(Unit) + if (resumedSemaphore.availablePermits() > 0) + return + resumedSemaphore.release() + } + + override fun sessionStopped() { + stoppedEvent.fire(Unit) + if (stoppedSemaphore.availablePermits() > 0) + return + stoppedSemaphore.release() + } + + override fun stackFrameChanged() { + super.stackFrameChanged() + } + + override fun beforeSessionResume() { + super.beforeSessionResume() + } + + override fun settingsChanged() { + super.settingsChanged() + } + + override fun breakpointsMuted(muted: Boolean) { + super.breakpointsMuted(muted) + } +} diff --git a/src/test/kotlin/com/intellij/plugin/powershell/debugger/StepTest.kt b/src/test/kotlin/com/intellij/plugin/powershell/debugger/StepTest.kt new file mode 100644 index 00000000..fd0927d7 --- /dev/null +++ b/src/test/kotlin/com/intellij/plugin/powershell/debugger/StepTest.kt @@ -0,0 +1,89 @@ +package com.intellij.plugin.powershell.debugger + +import com.intellij.plugin.powershell.ide.debugger.PowerShellSuspendContext +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.testFramework.fixtures.TempDirTestFixture +import com.intellij.testFramework.fixtures.impl.TempDirTestFixtureImpl +import com.intellij.xdebugger.XDebuggerTestUtil +import junit.framework.TestCase + +class StepTest: BasePlatformTestCase() { + + override fun getTestDataPath() = "src/test/resources/testData" + + override fun createTempDirTestFixture(): TempDirTestFixture { + return TempDirTestFixtureImpl() + } + + fun testStepOver() + { + val psiFile = myFixture.configureByFile("debugger/stepTest.ps1") + val file = psiFile.virtualFile + + val fileLine = 11 // line in file, starting from 1 + val line = fileLine - 1 // breakpoint line, starting from 0 + + val testSession = PowerShellTestSession(project, file.toNioPath()) + XDebuggerTestUtil.toggleBreakpoint(project, file, line) + val debugSession = testSession.startDebugSession() + XDebuggerTestUtil.waitFor(testSession.sessionListener.pausedSemaphore, testSession.waitForBackgroundTimeout.toMillis()) + val suspendContext = debugSession.suspendContext as PowerShellSuspendContext + TestCase.assertEquals(line, suspendContext.activeExecutionStack.topFrame?.sourcePosition?.line) + debugSession.stepOver(false) + XDebuggerTestUtil.waitFor(testSession.sessionListener.pausedSemaphore, testSession.waitForBackgroundTimeout.toMillis()) + TestCase.assertEquals(line + 1, (debugSession.suspendContext as PowerShellSuspendContext).activeExecutionStack.topFrame?.sourcePosition?.line) + + myFixture.projectDisposable.dispose() + } + + fun testStepIn() + { + val psiFile = myFixture.configureByFile("debugger/stepTest.ps1") + val file = psiFile.virtualFile + + val fileLine = 11 // line in file, starting from 1 + val line = fileLine - 1 // breakpoint line, starting from 0 + + val stepInLine = 0 + + val testSession = PowerShellTestSession(project, file.toNioPath()) + XDebuggerTestUtil.toggleBreakpoint(project, file, line) + val debugSession = testSession.startDebugSession() + XDebuggerTestUtil.waitFor(testSession.sessionListener.pausedSemaphore, testSession.waitForBackgroundTimeout.toMillis()) + val suspendContext = debugSession.suspendContext as PowerShellSuspendContext + TestCase.assertEquals(line, suspendContext.activeExecutionStack.topFrame?.sourcePosition?.line) + debugSession.stepInto() + XDebuggerTestUtil.waitFor(testSession.sessionListener.pausedSemaphore, testSession.waitForBackgroundTimeout.toMillis()) + TestCase.assertEquals(stepInLine, (debugSession.suspendContext as PowerShellSuspendContext).activeExecutionStack.topFrame?.sourcePosition?.line) + + myFixture.projectDisposable.dispose() + } + + fun testStepOut() + { + val psiFile = myFixture.configureByFile("debugger/stepTest.ps1") + val file = psiFile.virtualFile + + val fileLine = 7 // line in file, starting from 1 + val line = fileLine - 1 // breakpoint line, starting from 0 + + val stepOutFileLine = 12 + val stepOutLine = stepOutFileLine - 1 + + val testSession = PowerShellTestSession(project, file.toNioPath()) + XDebuggerTestUtil.toggleBreakpoint(project, file, line) + val debugSession = testSession.startDebugSession() + XDebuggerTestUtil.waitFor(testSession.sessionListener.pausedSemaphore, testSession.waitForBackgroundTimeout.toMillis()) + val suspendContext = debugSession.suspendContext as PowerShellSuspendContext + TestCase.assertEquals(line, suspendContext.activeExecutionStack.topFrame?.sourcePosition?.line) + debugSession.stepOut() + XDebuggerTestUtil.waitFor(testSession.sessionListener.pausedSemaphore, testSession.waitForBackgroundTimeout.toMillis()) + TestCase.assertEquals(stepOutLine, (debugSession.suspendContext as PowerShellSuspendContext).activeExecutionStack.topFrame?.sourcePosition?.line) + + myFixture.projectDisposable.dispose() + } + + override fun tearDown() { + myFixture.tearDown() + } +} diff --git a/src/test/kotlin/com/intellij/plugin/powershell/debugger/VariableTest.kt b/src/test/kotlin/com/intellij/plugin/powershell/debugger/VariableTest.kt new file mode 100644 index 00000000..34f591c5 --- /dev/null +++ b/src/test/kotlin/com/intellij/plugin/powershell/debugger/VariableTest.kt @@ -0,0 +1,135 @@ +package com.intellij.plugin.powershell.debugger + +import com.intellij.openapi.util.Pair +import com.intellij.plugin.powershell.PowerShellDebuggerTestUtil +import com.intellij.plugin.powershell.ide.debugger.PowerShellSuspendContext +import com.intellij.plugin.powershell.lang.PowerShellLanguage +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.testFramework.fixtures.TempDirTestFixture +import com.intellij.testFramework.fixtures.impl.TempDirTestFixtureImpl +import com.intellij.xdebugger.XDebuggerTestUtil +import com.intellij.xdebugger.XTestCompositeNode +import com.intellij.xdebugger.XTestEvaluationCallback +import com.intellij.xdebugger.frame.XValue +import com.intellij.xdebugger.frame.XValueModifier +import com.intellij.xdebugger.frame.XValueModifier.XModificationCallback +import com.intellij.xdebugger.impl.breakpoints.XExpressionImpl +import com.intellij.xdebugger.impl.ui.tree.nodes.XEvaluationCallbackBase +import junit.framework.TestCase +import org.junit.Assert +import java.util.concurrent.Semaphore + +class VariableTest: BasePlatformTestCase() { + override fun getTestDataPath() = "src/test/resources/testData" + + override fun createTempDirTestFixture(): TempDirTestFixture { + return TempDirTestFixtureImpl() + } + + fun testPrimitiveVariable() { + val psiFile = myFixture.configureByFile("debugger/variableTest.ps1") + val file = psiFile.virtualFile + + val fileLine = 2 // line in file, starting from 1 + val line = fileLine - 1 // breakpoint line, starting from 0 + val variableName = "\$myPrimitiveVar" + val value = 69 + + val testSession = PowerShellTestSession(project, file.toNioPath()) + XDebuggerTestUtil.toggleBreakpoint(project, file, line) + val debugSession = testSession.startDebugSession() + XDebuggerTestUtil.waitFor(testSession.sessionListener.pausedSemaphore, testSession.waitForBackgroundTimeout.toMillis()) + val suspendContext = debugSession.suspendContext as PowerShellSuspendContext + val topFrame = suspendContext.activeExecutionStack.topFrame!! + val children = XTestCompositeNode(topFrame).collectChildren() + val variableValue = XDebuggerTestUtil.findVar(children, variableName) + val variableValueNode = XDebuggerTestUtil.computePresentation(variableValue) + TestCase.assertEquals(value.toString(), variableValueNode.myValue) + myFixture.projectDisposable.dispose() + } + + fun testComplexVariable() + { + val psiFile = myFixture.configureByFile("debugger/variableTest.ps1") + val file = psiFile.virtualFile + + val fileLine = 5 // line in file, starting from 1 + val line = fileLine - 1 // breakpoint line, starting from 0 + val variableName = "\$myComplexVar" + val nestedFieldName = "NestedField" + val nestedFieldValue = 123 + + val testSession = PowerShellTestSession(project, file.toNioPath()) + val millis = testSession.waitForBackgroundTimeout.toMillis() + + XDebuggerTestUtil.toggleBreakpoint(project, file, line) + val debugSession = testSession.startDebugSession() + XDebuggerTestUtil.waitFor(testSession.sessionListener.pausedSemaphore, millis) + val suspendContext = debugSession.suspendContext as PowerShellSuspendContext + val topFrame = suspendContext.activeExecutionStack.topFrame!! + val children = XTestCompositeNode(topFrame).collectChildren(millis) + val variableValue = XDebuggerTestUtil.findVar(children, variableName) + val variableChildren = XTestCompositeNode(variableValue).collectChildren(millis) + TestCase.assertTrue("variableChildren.size (${variableChildren.size}) is not greater than zero", variableChildren.size > 0) + val nestedField = XDebuggerTestUtil.findVar(variableChildren, nestedFieldName) + val nestedFieldValueNode = XDebuggerTestUtil.computePresentation(nestedField) + TestCase.assertEquals(nestedFieldValue.toString(), nestedFieldValueNode.myValue) + myFixture.projectDisposable.dispose() + } + + fun testSetVariable() + { + val psiFile = myFixture.configureByFile("debugger/variableTest.ps1") + val file = psiFile.virtualFile + + val fileLine = 2 // line in file, starting from 1 + val line = fileLine - 1 // breakpoint line, starting from 0 + val secondfileLine = 5 // line in file, starting from 1 + val secondLine = secondfileLine - 1 // breakpoint line, starting from 0 + + val variableName = "\$myPrimitiveVar" + val value = 123 + val setValueExpression = XExpressionImpl("$value", PowerShellLanguage.INSTANCE, "") + + val testSession = PowerShellTestSession(project, file.toNioPath()) + val millis = testSession.waitForBackgroundTimeout.toMillis() + + XDebuggerTestUtil.toggleBreakpoint(project, file, line) + XDebuggerTestUtil.toggleBreakpoint(project, file, secondLine) + + val debugSession = testSession.startDebugSession() + XDebuggerTestUtil.waitFor(testSession.sessionListener.pausedSemaphore, millis) + val suspendContext = debugSession.suspendContext as PowerShellSuspendContext + + val variableValue = PowerShellDebuggerTestUtil.getVariable(suspendContext, variableName) + var callback = XTestModificationCallback() + variableValue.modifier!!.setValue(setValueExpression, callback) + callback.waitFor(millis) + debugSession.resume() + XDebuggerTestUtil.waitFor(testSession.sessionListener.pausedSemaphore, testSession.waitForBackgroundTimeout.toMillis()) + val resultVariable = PowerShellDebuggerTestUtil.getVariable(debugSession.suspendContext as PowerShellSuspendContext, variableName) + val variableValueNode = XDebuggerTestUtil.computePresentation(resultVariable) + TestCase.assertEquals(value.toString(), variableValueNode.myValue) + myFixture.projectDisposable.dispose() + } +} + +class XTestModificationCallback : XValueModifier.XModificationCallback { + private var myErrorMessage: String? = null + private val myFinished = Semaphore(0) + + + override fun errorOccurred(errorMessage: String) { + myErrorMessage = errorMessage + myFinished.release() + } + + override fun valueModified() { + myFinished.release() + } + + fun waitFor(timeoutInMilliseconds: Long): Pair { + Assert.assertTrue("timed out", XDebuggerTestUtil.waitFor(myFinished, timeoutInMilliseconds)) + return Pair.create(true, myErrorMessage) + } +} diff --git a/src/test/resources/testData/debugger/stepTest.ps1 b/src/test/resources/testData/debugger/stepTest.ps1 new file mode 100644 index 00000000..440323b7 --- /dev/null +++ b/src/test/resources/testData/debugger/stepTest.ps1 @@ -0,0 +1,13 @@ +function Add-Numbers { + param( + [double]$num1, + [double]$num2 + ) + + $result = $num1 + $num2 + return $result +} +Write-Output "Starting the script..." +$result = Add-Numbers -num1 10 -num2 5 +Write-Output "The result is: $result" +Write-Output "End of the script." diff --git a/src/test/resources/testData/debugger/testBreakpoint.ps1 b/src/test/resources/testData/debugger/testBreakpoint.ps1 new file mode 100644 index 00000000..66c7a012 --- /dev/null +++ b/src/test/resources/testData/debugger/testBreakpoint.ps1 @@ -0,0 +1,7 @@ +$val = 0 +$result = Get-Variable +while($val -ne 3) +{ + $val++ + Write-Host $val +} diff --git a/src/test/resources/testData/debugger/variableTest.ps1 b/src/test/resources/testData/debugger/variableTest.ps1 new file mode 100644 index 00000000..1ef4f6d8 --- /dev/null +++ b/src/test/resources/testData/debugger/variableTest.ps1 @@ -0,0 +1,5 @@ +$myPrimitiveVar = 69 +$myComplexVar = [PSCustomObject]@{ + NestedField = 123 + } +Write-Host "Variable Test"