From 14358c584d5b638ac1bef3de1e99f87ce4f68e6e Mon Sep 17 00:00:00 2001 From: Phantom Date: Mon, 15 Jul 2024 19:56:26 +0400 Subject: [PATCH] Debugger WIP 16 Add Variables cache see https://github.com/PowerShell/PowerShellEditorServices/pull/2169 Debugger WIP 15 Catch value modification errors Debugger WIP 15 Debugger eval completion Debugger WIP 14 Add id to document Debugger WIP 13 Remove useless files Debugger WIP 12 Add Variable Test Debugger WIP 11.2 Remove useless comments Debugger WIP 11.1 Change hardcoded parameter to variable remove unused variable from EvaluationTest.testEvaluation Debugger WIP 11 Call clientSession.terminateDebugging() on dispose Change PowerShellDebuggerVariableValue supertype to XNamedValue update formatting Test: Update BreakpointTest Add EvaluationTest Add StepTest Debugger WIP 10 Add tests Add BreakpointTest Debugger WIP 9 Set variable value Debugger WIP 8 run project file instead hardcoded file set all breakpoints on start Debugger WIP 7 Step Over/In/Out support Terminate support Debugger WIP 6 Conditional and log support Debugger WIP 5 IDE Breakpoints support Resume (continue) support Debugger WIP 5 Variable list Complex objects Debugger WIP 3 variable list eval Debugger WIP 2 Debugger WIP --- build.gradle.kts | 1 + debugger.ps1 | 24 -- .../ide/actions/PowerShellStartDebugAction.kt | 48 --- .../debugger/PowerShellBreakpointHandler.kt | 20 +- .../ide/debugger/PowerShellBreakpointType.kt | 93 +++++- .../ide/debugger/PowerShellDebugProcess.kt | 64 +++- .../ide/debugger/PowerShellDebugSession.kt | 314 ++++++++++++++++++ .../PowerShellDebuggerEditorsProvider.kt | 20 +- .../debugger/PowershellDebuggerEvaluator.kt | 35 ++ .../PowerShellCompletionContributor.kt | 3 + ...PowerShellDebuggerCompletionContributor.kt | 276 +++++++++++++++ .../ide/run/PowerShellProgramDebugRunner.kt | 15 +- .../run/PowerShellScriptCommandLineState.kt | 120 ++++++- .../powershell/lang/debugger/PSDebugClient.kt | 89 +++++ .../EditorServicesLanguageHostStarter.kt | 79 ++++- .../LanguageHostConnectionManager.kt | 1 + src/main/resources/META-INF/plugin.xml | 8 +- .../powershell/PowerShellDebuggerTestUtil.kt | 21 ++ .../powershell/debugger/BreakpointTest.kt | 71 ++++ .../powershell/debugger/EvaluationTest.kt | 36 ++ .../debugger/PowerShellTestSession.kt | 127 +++++++ .../plugin/powershell/debugger/StepTest.kt | 89 +++++ .../powershell/debugger/VariableTest.kt | 135 ++++++++ .../resources/testData/debugger/stepTest.ps1 | 13 + .../testData/debugger/testBreakpoint.ps1 | 7 + .../testData/debugger/variableTest.ps1 | 5 + 26 files changed, 1592 insertions(+), 122 deletions(-) delete mode 100644 debugger.ps1 delete mode 100644 src/main/kotlin/com/intellij/plugin/powershell/ide/actions/PowerShellStartDebugAction.kt create mode 100644 src/main/kotlin/com/intellij/plugin/powershell/ide/debugger/PowerShellDebugSession.kt create mode 100644 src/main/kotlin/com/intellij/plugin/powershell/ide/debugger/PowershellDebuggerEvaluator.kt create mode 100644 src/main/kotlin/com/intellij/plugin/powershell/ide/editor/completion/PowerShellDebuggerCompletionContributor.kt create mode 100644 src/main/kotlin/com/intellij/plugin/powershell/lang/debugger/PSDebugClient.kt create mode 100644 src/test/kotlin/com/intellij/plugin/powershell/PowerShellDebuggerTestUtil.kt create mode 100644 src/test/kotlin/com/intellij/plugin/powershell/debugger/BreakpointTest.kt create mode 100644 src/test/kotlin/com/intellij/plugin/powershell/debugger/EvaluationTest.kt create mode 100644 src/test/kotlin/com/intellij/plugin/powershell/debugger/PowerShellTestSession.kt create mode 100644 src/test/kotlin/com/intellij/plugin/powershell/debugger/StepTest.kt create mode 100644 src/test/kotlin/com/intellij/plugin/powershell/debugger/VariableTest.kt create mode 100644 src/test/resources/testData/debugger/stepTest.ps1 create mode 100644 src/test/resources/testData/debugger/testBreakpoint.ps1 create mode 100644 src/test/resources/testData/debugger/variableTest.ps1 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..1a9f727f --- /dev/null +++ b/src/test/kotlin/com/intellij/plugin/powershell/debugger/PowerShellTestSession.kt @@ -0,0 +1,127 @@ +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() + + //val scriptPath = """C:\Users\Sergey.Vardanyan\IdeaProjects\powershellTest\test.ps1""" //PathManagerEx.getTestDataPath("/debugger/") + 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"