diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index c8375dfe52..0000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,127 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - // NOTE: These are not in the code-workspace file because StartDebugging cannot current resolve configs stored there so they have to be here for the Mocha Test Explorer feature. - // Ref: https://github.com/microsoft/vscode/issues/150663 - { - "name": "Launch Extension", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" - ], - "env": { - "__TEST_WORKSPACE_PATH": "${workspaceFolder}/examples", - }, - "sourceMaps": true, - // This speeds up source map detection and makes smartStep work correctly - "outFiles": [ - "${workspaceFolder}/**/*.js", - "!**/node_modules/**", - "!**/.vscode-test/**" - ], - "skipFiles": [ - "/**", - "**/node_modules/**", - "**/.vscode-test/**" - ], - "presentation": { - "hidden": false, - "group": "test", - "order": 2 - } - }, - { - // Runs the extension in an empty temp profile that is automatically cleaned up after use - // Undocumented: https://github.com/microsoft/vscode-docs/issues/6220 - "name": "Launch Extension - Temp Profile", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": [ - "--profile-temp", - "--extensionDevelopmentPath=${workspaceFolder}", - "${workspaceFolder}/examples" - ], - "sourceMaps": true, - // This speeds up source map detection and makes smartStep work correctly - "outFiles": [ - "${workspaceFolder}/**/*.js", - "!**/node_modules/**", - "!**/.vscode-test/**" - ], - "skipFiles": [ - "/**", - "**/node_modules/**", - "**/.vscode-test/**" - ], - "presentation": { - "hidden": false, - "group": "test", - "order": 2 - } - }, - { - // Runs the extension in an isolated but persistent profile separate from the user settings - // Undocumented: https://github.com/microsoft/vscode-docs/issues/6220 - "name": "Launch Extension - Isolated Profile", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": [ - "--profile=debug", - "--extensionDevelopmentPath=${workspaceFolder}", - "${workspaceFolder}/examples" - ], - "sourceMaps": true, - // This speeds up source map detection and makes smartStep work correctly - "outFiles": [ - "${workspaceFolder}/**/*.js", - "!**/node_modules/**", - "!**/.vscode-test/**" - ], - "skipFiles": [ - "/**", - "**/node_modules/**", - "**/.vscode-test/**" - ], - "presentation": { - "hidden": false, - "group": "test", - "order": 2 - } - }, - { - "name": "Test Extension", - "type": "node", - "request": "launch", - "program": "${workspaceFolder}/test/runTests.js", - "cascadeTerminateToConfigurations": [ - "ExtensionTests", - ], - // This speeds up source map detection and makes smartStep work correctly - "outFiles": [ - "${workspaceFolder}/**/*.js", - "!**/node_modules/**", - "!**/.vscode-test/**" - ], - "skipFiles": [ - "/**", - "**/node_modules/**", - "**/.vscode-test/**" - ], - "attachSimplePort": 59229, // The default is 9229 but we want to avoid conflicts because we will have two Code instances running. - "env": { - "__TEST_DEBUG_INSPECT_PORT": "59229" // Needs to match attachSimplePort - }, - "presentation": { - "hidden": false, - }, - "internalConsoleOptions": "neverOpen", - "console": "integratedTerminal", - "autoAttachChildProcesses": false, - "preLaunchTask": "watch-tests" - } - ] -} diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index fa53736684..0000000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "watch-tests", - "icon": { - "color": "terminal.ansiCyan", - "id": "sync" - }, - "type": "npm", - "script": "watch-tests", - "group": "test", - "problemMatcher": "$tsc-watch", - "isBackground": true, - "dependsOn": "watch" - }, - { - "label": "watch", - "icon": { - "color": "terminal.ansiCyan", - "id": "sync" - }, - "type": "npm", - "script": "watch", - "group": "build", - "problemMatcher": "$esbuild-watch", - "isBackground": true - } - ] -} diff --git a/docs/development.md b/docs/development.md index 28b55750ab..663851e14a 100644 --- a/docs/development.md +++ b/docs/development.md @@ -56,10 +56,19 @@ Invoke-Build Build Explore the `vscode-powershell.build.ps1` file for other build targets. ### Launching the extension +First, ensure you have completed a build as instructed above, as the launch templates do not check some prerequisites for performance reasons. -To debug the extension use one of the provided `Launch Extension` debug configurations (remember to rebuild first). -You can simultaneously use the `Attach to Editor Services` configuration to attach the .NET debugger to the PowerShell process running the server. -Try the `powershell.developer.editorServicesWaitForDebugger` setting to attach before startup. +To debug the extension use one of the provided `Launch Extension` debug configurations. +1. `Launch Extension`: Launches the debugger using your personal profile settings +1. `Temp Profile`: Launches VSCode with an ephemeral temp profile that resets on every launch. Useful for "out of the box" environment testing. +1. `Isolated Profile`: Launches the debugger with a persistent debug profile specific to the extension, so you can preserve some settings or test certain prerequisites. + +All three templates use pre-launch tasks to build the code, and support automatic restart of the extension host on changes to the Extension source code. [Hot Reload](https://devblogs.microsoft.com/dotnet/introducing-net-hot-reload/) is also enabled for PowerShell Editor Services. + +> [!WARNING] +> There is a current limitation that, if you restart the extension/extension host or it is restarted due to a extension code change, the editor services attachment will be disconnected due to the PSES terminal being terminated, and you will either need to restart the debug session completely, or do a manual build of PSES and run the `Attach to Editor Services` debug launch manually. + +Try the `powershell.developer.editorServicesWaitForDebugger` setting to ensure that you are fully attached before the extension startup process continues. ## Contributing Snippets diff --git a/extension-dev.code-workspace b/extension-dev.code-workspace deleted file mode 100644 index d4e7a6aa4a..0000000000 --- a/extension-dev.code-workspace +++ /dev/null @@ -1,231 +0,0 @@ -{ - "folders": [ - { - "name": "Client", - "path": "." - }, - { - "name": "Server", - "path": "../PowerShellEditorServices" - } - ], - "extensions": { - "recommendations": [ - "davidanson.vscode-markdownlint", - "dbaeumer.vscode-eslint", - "editorconfig.editorconfig", - "josefpihrt-vscode.roslynator", - "ms-azure-devops.azure-pipelines", - "ms-dotnettools.csharp", - "ms-vscode.powershell", - "hbenl.vscode-mocha-test-adapter", - "connor4312.esbuild-problem-matchers" - ] - }, - "settings": { - "window.title": "PowerShell VS Code Extension Development", - "debug.onTaskErrors": "prompt", - "editor.tabSize": 4, - "editor.insertSpaces": true, - "files.trimTrailingWhitespace": true, - "files.insertFinalNewline": true, - "files.associations": { - "**/snippets/*.json": "jsonc", // Use JSONC instead of JSON because that's how VS Code interprets snippet files, and it enables better source documentation. - "**/.vsts-ci/**/*.yml": "azure-pipelines", - }, - // Ignore the Markdown rule: - "markdownlint.config": { - "MD024": false // no-duplicate-header - }, - "powershell.cwd": "Client", - "powershell.codeFormatting.autoCorrectAliases": true, - "powershell.codeFormatting.avoidSemicolonsAsLineTerminators": true, - "powershell.codeFormatting.newLineAfterCloseBrace": false, - "powershell.codeFormatting.trimWhitespaceAroundPipe": true, - "powershell.codeFormatting.useCorrectCasing": true, - "powershell.codeFormatting.whitespaceBeforeOpenBrace": false, - "powershell.codeFormatting.whitespaceBetweenParameters": true, - "powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationForFirstPipeline", - "typescript.tsdk": "Client/node_modules/typescript/lib", // Lock the TypeScript SDK path to the version we use - "typescript.format.semicolons": "insert", // Code actions like "organize imports" ignore ESLint, so we need this here - "eslint.format.enable": true, // Enable ESLint as defaut formatter so quick fixes can be applied directly - "[typescript]": { - "editor.defaultFormatter": "dbaeumer.vscode-eslint", - "editor.formatOnPaste": true, - "editor.formatOnSave": true, - "editor.formatOnSaveMode": "modificationsIfAvailable" - }, - "mochaExplorer.configFile": ".mocharc.json", - "mochaExplorer.launcherScript": "test/runTests", - "mochaExplorer.autoload": false, // The test instance pops up every time discovery or run is done, this could be annoying on startup. - "mochaExplorer.debuggerPort": 59229, // Matches the launch config, we dont want to use the default port as we are launching a duplicate instance of vscode and it might conflict. - "mochaExplorer.ipcRole": "server", - "mochaExplorer.ipcTimeout": 30000, // 30 seconds - "testExplorer.useNativeTesting": true, - "mochaExplorer.env": { - "VSCODE_VERSION": "insiders", - "ELECTRON_RUN_AS_NODE": null - } - }, - "tasks": { - "version": "2.0.0", - "windows": { - "options": { - "shell": { - "executable": "pwsh.exe", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-Command" - ] - } - } - }, - "linux": { - "options": { - "shell": { - "executable": "pwsh", - "args": [ - "-NoProfile", - "-Command" - ] - } - } - }, - "osx": { - "options": { - "shell": { - "executable": "/usr/local/bin/pwsh", - "args": [ - "-NoProfile", - "-Command" - ] - } - } - }, - "tasks": [ - { - "label": "Build", - "type": "shell", - "options": { - "cwd": "${workspaceFolder:Client}" - }, - "command": "Invoke-Build Build", - "problemMatcher": [ - "$msCompile", - "$tsc" - ], - "group": { - "kind": "build", - "isDefault": true - } - }, - { - "label": "Test Client", - "type": "shell", - "options": { - "cwd": "${workspaceFolder:Client}" - }, - "command": "Invoke-Build Test", - "problemMatcher": [ - "$msCompile", - "$tsc" - ], - "group": { - "kind": "test", - "isDefault": true - } - }, - { - "label": "Test Server", - "type": "shell", - "options": { - "cwd": "${workspaceFolder:Server}" - }, - "problemMatcher": [ - "$msCompile" - ], - "command": "Invoke-Build TestPS74", - "group": { - "kind": "test", - "isDefault": true - } - }, - { - "label": "Invoke-Build Client", - "type": "shell", - "options": { - "cwd": "${workspaceFolder:Client}" - }, - "command": "Invoke-Build ${input:clientBuildCommand}", - "group": "build" - }, - { - "label": "Invoke-Build Server", - "type": "shell", - "options": { - "cwd": "${workspaceFolder:Server}" - }, - "command": "Invoke-Build ${input:serverBuildCommand}", - "group": "build" - } - ], - "inputs": [ - { - "type": "pickString", - "id": "clientBuildCommand", - "description": "Which Invoke-Build Client Task?", - "options": [ - "Restore", - "Clean", - "Build", - "Test", - "Package" - ], - "default": "Clean" - }, - { - "type": "pickString", - "id": "serverBuildCommand", - "description": "Which Invoke-Build Server Task?", - "options": [ - "SetupDotNet", - "BinClean", - "Clean", - "Build", - "Test", - "TestPS74", - "TestE2EPwsh", - "TestPS51", - "TestE2EPowerShell", - ], - "default": "Clean" - } - ] - }, - "launch": { - "version": "0.2.0", - "configurations": [ - { - // https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md - "name": "Attach to Editor Services", - "type": "coreclr", - "request": "attach", - "processId": "${command:pickProcess}", - "justMyCode": false, - "suppressJITOptimizations": true, - "symbolOptions": { - "searchPaths": [], - "searchMicrosoftSymbolServer": true, - "searchNuGetOrgSymbolServer": true - }, - "presentation": { - "hidden": false, - "group": "test", - "order": 3 - } - } - ] - } -} diff --git a/package.json b/package.json index bb448383fb..604edb2657 100644 --- a/package.json +++ b/package.json @@ -300,7 +300,19 @@ "title": "Move panel to bottom", "category": "PowerShell", "icon": "$(layout-panel-right)" + }, + { + "title": "[PowerShell Debug Input Task] Wait for and return PSES startup process ID", + "command": "PowerShell.WaitForPsesActivationAndReturnProcessId", + "enablement": "false" + }, + { + "title": "[PowerShell Debug Input Task] Get the VSCode Session ID for writing the PID file", + "command": "GetVsCodeSessionId", + "enablement": "false" } + + ], "menus": { "commandPalette": [ diff --git a/pwsh-extension-dev.code-workspace b/pwsh-extension-dev.code-workspace new file mode 100644 index 0000000000..1986a3a969 --- /dev/null +++ b/pwsh-extension-dev.code-workspace @@ -0,0 +1,493 @@ +{ + "folders": [ + { + "name": "Client", + "path": "." + }, + { + "name": "Server", + "path": "../PowerShellEditorServices" + } + ], + + "extensions": { + "recommendations": [ + "davidanson.vscode-markdownlint", + "dbaeumer.vscode-eslint", + "editorconfig.editorconfig", + "ms-azure-devops.azure-pipelines", + "ms-dotnettools.csharp", + "ms-vscode.powershell", + "hbenl.vscode-mocha-test-adapter", + "connor4312.esbuild-problem-matchers" + ] + }, + "settings": { + "window.title": "PowerShell VS Code Extension Development", + "debug.onTaskErrors": "prompt", + "editor.tabSize": 4, + "editor.insertSpaces": true, + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + "files.associations": { + "**/snippets/*.json": "jsonc", // Use JSONC instead of JSON because that's how VS Code interprets snippet files, and it enables better source documentation. + "**/.vsts-ci/**/*.yml": "azure-pipelines", + }, + // Ignore the Markdown rule: + "markdownlint.config": { + "MD024": false // no-duplicate-header + }, + "powershell.cwd": "Client", + "powershell.codeFormatting.autoCorrectAliases": true, + "powershell.codeFormatting.avoidSemicolonsAsLineTerminators": true, + "powershell.codeFormatting.newLineAfterCloseBrace": false, + "powershell.codeFormatting.trimWhitespaceAroundPipe": true, + "powershell.codeFormatting.useCorrectCasing": true, + "powershell.codeFormatting.whitespaceBeforeOpenBrace": false, + "powershell.codeFormatting.whitespaceBetweenParameters": true, + "powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationForFirstPipeline", + "typescript.tsdk": "Client/node_modules/typescript/lib", // Lock the TypeScript SDK path to the version we use + "typescript.format.semicolons": "insert", // Code actions like "organize imports" ignore ESLint, so we need this here + "eslint.format.enable": true, // Enable ESLint as defaut formatter so quick fixes can be applied directly + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnPaste": true, + "editor.formatOnSave": true, + "editor.formatOnSaveMode": "modificationsIfAvailable" + }, + "mochaExplorer.configFile": ".mocharc.json", + "mochaExplorer.launcherScript": "test/runTests", + "mochaExplorer.autoload": false, // The test instance pops up every time discovery or run is done, this could be annoying on startup. + "mochaExplorer.debuggerPort": 59229, // Matches the launch config, we dont want to use the default port as we are launching a duplicate instance of vscode and it might conflict. + "mochaExplorer.ipcRole": "server", + "mochaExplorer.ipcTimeout": 30000, // 30 seconds + "testExplorer.useNativeTesting": true, + "mochaExplorer.env": { + "VSCODE_VERSION": "insiders", + "ELECTRON_RUN_AS_NODE": null + } + }, + "tasks": { + "version": "2.0.0", + "windows": { + "options": { + "shell": { + "executable": "pwsh.exe", + "args": [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command" + ] + } + } + }, + "linux": { + "options": { + "shell": { + "executable": "pwsh", + "args": [ + "-NoProfile", + "-Command" + ] + } + } + }, + "osx": { + "options": { + "shell": { + "executable": "/usr/local/bin/pwsh", + "args": [ + "-NoProfile", + "-Command" + ] + } + } + }, + "tasks": [ + { + "label": "Build", + "icon": { + "id": "tools", + }, + "type": "shell", + "options": { + "cwd": "${workspaceFolder:Client}" + }, + "command": "Invoke-Build Build", + "problemMatcher": [ + "$msCompile", + "$tsc" + ], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "Extension: Watch", + "type": "shell", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder:Client}" + }, + "command": "npm run watch", + "problemMatcher": { + "owner": "esbuild-watch", + "pattern": { + "regexp": "^\\[ERROR\\] (.+)\\n\\n\\s+([^:]+):(\\d+):(\\d+):", + "message": 1, + "file": 2, + "line": 3, + "column": 4, + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^\\[watch\\] build started", + "endsPattern": "^\\[watch\\] build finished" + } + }, + "icon": { + "id": "sync", + "color": "terminal.ansiCyan" + }, + "group": { + "kind": "build", + } + }, + { + "label": "PSES: BuildIfChanged", + "type": "shell", + "options": { + "cwd": "${workspaceFolder:Server}" + }, + "command": "Invoke-Build BuildIfChanged", + "problemMatcher": "$msCompile", + "icon": { + "id": "tools", + "color": "terminal.ansiCyan" + }, + "group": { + "kind": "build", + } + }, + { + "label": "PreLaunch", + "dependsOn": [ + "Extension: Watch", + "PSES: BuildIfChanged" + ], + "dependsOrder": "parallel" + }, + { + "label": "Test Client", + "type": "shell", + "options": { + "cwd": "${workspaceFolder:Client}" + }, + "command": "Invoke-Build Test", + "problemMatcher": [ + "$msCompile", + "$tsc" + ], + "group": { + "kind": "test", + "isDefault": true + } + }, + { + "label": "Test Server", + "type": "shell", + "options": { + "cwd": "${workspaceFolder:Server}" + }, + "problemMatcher": [ + "$msCompile" + ], + "command": "Invoke-Build TestPS74", + "group": { + "kind": "test", + "isDefault": true + } + }, + { + "label": "Invoke-Build Client", + "type": "shell", + "options": { + "cwd": "${workspaceFolder:Client}" + }, + "command": "Invoke-Build ${input:clientBuildCommand}", + "group": "build" + }, + { + "label": "Invoke-Build Server", + "type": "shell", + "options": { + "cwd": "${workspaceFolder:Server}" + }, + "command": "Invoke-Build ${input:serverBuildCommand}", + "group": "build" + } + ], + "inputs": [ + { + "type": "pickString", + "id": "clientBuildCommand", + "description": "Which Invoke-Build Client Task?", + "options": [ + "Restore", + "Clean", + "Build", + "Test", + "Package" + ], + "default": "Clean" + }, + { + "type": "pickString", + "id": "serverBuildCommand", + "description": "Which Invoke-Build Server Task?", + "options": [ + "SetupDotNet", + "BinClean", + "Clean", + "Build", + "Test", + "TestPS74", + "TestE2EPwsh", + "TestPS51", + "TestE2EPowerShell", + ], + "default": "Clean" + } + ], + + }, + "launch": { + "version": "0.2.0", + "compounds": [ + { + "name": "Launch Extension", + "configurations": [ + "Launch", + "PowerShell Editor Services" + ], + "preLaunchTask": "Build", + "stopAll": true, + "presentation": { + "hidden": false, + "group": "Test", + "order": 1 + } + }, + { + "name": "Launch Extension - Temp Profile", + "configurations": [ + "Launch-Temp", + "PowerShell Editor Services" + ], + "preLaunchTask": "PreLaunch", + "stopAll": true, + "presentation": { + "hidden": false, + "group": "Test", + "order": 2 + } + }, + { + "name": "Launch Extension - Isolated Profile", + "configurations": [ + "Launch-Isolated", + "PowerShell Editor Services" + ], + "preLaunchTask": "Build", + "stopAll": true, + "presentation": { + "hidden": false, + "group": "Test", + "order": 3 + } + }, + { + "name": "Interactively Test Rename", + "configurations": [ + "Launch Extension - Rename Test Cases", + "Attach to Editor Services" + ], + "preLaunchTask": "Build", + "stopAll": true, + "presentation": { + "hidden": false, + "group": "Test", + "order": 4 + } + } + ], + "configurations": [ + { + "name": "Launch", + "type": "extensionHost", + "request": "launch", + "env": { + "VSCODE_PARENT_SESSION_ID": "${command:GetVsCodeSessionId}", + }, + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder:Client}", + "${workspaceFolder:Client}/examples" + ], + "sourceMaps": true, + // This speeds up source map detection and makes smartStep work correctly + "outFiles": [ + "${workspaceFolder:Client}/**/*.js", + "!**/node_modules/**", + "!**/.vscode-test/**" + ], + "skipFiles": [ + "/**", + "**/node_modules/**", + "**/.vscode-test/**", + "**/app/out/vs/**" //Skips Extension Host internals + ], + "presentation": { + "hidden": true + } + }, + { + "name": "Launch-Temp", + "type": "extensionHost", + "request": "launch", + "env": { + "VSCODE_PARENT_SESSION_ID": "${command:GetVsCodeSessionId}", + }, + "runtimeExecutable": "${execPath}", + "args": [ + // Runs the extension in an empty temp profile that is automatically cleaned up after use + // Undocumented: https://github.com/microsoft/vscode-docs/issues/6220 + "--profile-temp", + "--extensionDevelopmentPath=${workspaceFolder:Client}", + "${workspaceFolder:Client}/examples" + ], + "sourceMaps": true, + // This speeds up source map detection and makes smartStep work correctly + "outFiles": [ + "${workspaceFolder:Client}/**/*.js", + "!**/node_modules/**", + "!**/.vscode-test/**" + ], + "skipFiles": [ + "/**", + "**/node_modules/**", + "**/.vscode-test/**", + "**/app/out/vs/**" //Skips Extension Host internals + ], + "presentation": { + "hidden": true + } + }, + { + "name": "Launch-Isolated", + "type": "extensionHost", + "request": "launch", + "env": { + "VSCODE_PARENT_SESSION_ID": "${command:GetVsCodeSessionId}", + }, + "runtimeExecutable": "${execPath}", + "args": [ + // Runs the extension in an empty temp profile that is automatically cleaned up after use + // Undocumented: https://github.com/microsoft/vscode-docs/issues/6220 + "--profile=pwsh-debug", + "--extensionDevelopmentPath=${workspaceFolder:Client}", + "${workspaceFolder:Client}/examples" + ], + "sourceMaps": true, + // This speeds up source map detection and makes smartStep work correctly + "outFiles": [ + "${workspaceFolder:Client}/**/*.js", + "!**/node_modules/**", + "!**/.vscode-test/**", + ], + "skipFiles": [ + "/**", + "**/node_modules/**", + "**/.vscode-test/**", + "**/app/out/vs/**" //Skips Extension Host internals + ], + "presentation": { + "hidden": true + } + }, + { + // https://code.visualstudio.com/docs/csharp/debugger-settings + "name": "Attach to Editor Services", + "type": "coreclr", + "request": "attach", + // Waits for the extension terminal to become available and gets the PID, saves having to enter it manually. + "processId": "${command:PowerShell.PickPSHostProcess}", + "justMyCode": true, + "suppressJITOptimizations": true, + "symbolOptions": { + "searchPaths": [], + "searchMicrosoftSymbolServer": false, + "searchNuGetOrgSymbolServer": false + }, + "presentation": { + "hidden": false, + "group": "Test", + "order": 5 + }, + "logging": { + "moduleLoad": false + }, + }, + { + // https://code.visualstudio.com/docs/csharp/debugger-settings + "name": "PowerShell Editor Services", + "type": "coreclr", + "request": "attach", + // Waits for the extension terminal to become available and gets the PID, saves having to enter it manually. + "processId": "${command:PowerShell.WaitForPsesActivationAndReturnProcessId}", + "justMyCode": true, + "suppressJITOptimizations": true, + "symbolOptions": { + "searchPaths": [], + "searchMicrosoftSymbolServer": false, + "searchNuGetOrgSymbolServer": false + }, + "presentation": { + "hidden": true + }, + "logging": { + "moduleLoad": false + } + }, + { + // Runs the extension in an isolated but persistent profile separate from the user settings + // Undocumented: https://github.com/microsoft/vscode-docs/issues/6220 + "name": "Launch Extension - Rename Test Cases", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--profile=debug", + "--extensionDevelopmentPath=${workspaceFolder:Client}", + "${workspaceFolder:Server}/test/PowerShellEditorServices.Test.Shared/Refactoring" + ], + "sourceMaps": true, + // This speeds up source map detection and makes smartStep work correctly + "outFiles": [ + "${workspaceFolder:Client}/**/*.js", + "!**/node_modules/**", + "!**/.vscode-test/**" + ], + "skipFiles": [ + "/**", + "**/node_modules/**", + "**/.vscode-test/**" + ], + "presentation": { + "hidden": true, } + }, + ] + } +} diff --git a/src/extension.ts b/src/extension.ts index 8676724ab1..b1bc52ac8f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -25,7 +25,6 @@ import { SessionManager } from "./session"; import { getSettings } from "./settings"; import { PowerShellLanguageId } from "./utils"; import { LanguageClientConsumer } from "./languageClientConsumer"; - // The 1DS telemetry key, which is just shared among all Microsoft extensions // (and isn't sensitive). const TELEMETRY_KEY = "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255"; @@ -44,6 +43,9 @@ const documentSelector: DocumentSelector = [ export async function activate(context: vscode.ExtensionContext): Promise { logger = new Logger(); + if (context.extensionMode === vscode.ExtensionMode.Development) { + restartOnExtensionFileChanges(context); + } telemetryReporter = new TelemetryReporter(TELEMETRY_KEY); @@ -151,7 +153,13 @@ export async function activate(context: vscode.ExtensionContext): Promise {logger.showLogPanel();} - ) + ), + vscode.commands.registerCommand( + "GetVsCodeSessionId", + () => vscode.env.sessionId + ), + // Register a command that waits for the Externsion Terminal to be active. Can be used by .NET Attach Tasks + registerWaitForPsesActivationCommand(context) ]; const externalApi = new ExternalApiFeature(context, sessionManager, logger); @@ -184,6 +192,50 @@ export async function activate(context: vscode.ExtensionContext): Promise { + const pidFileName = `PSES-${vscode.env.sessionId}.pid`; + const pidFile = vscode.Uri.joinPath(context.globalStorageUri, "sessions", pidFileName); + const fs = vscode.workspace.fs; + // Wait for the file to be created + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, no-constant-condition + while (true) { + try { + const pidContent = await fs.readFile(pidFile); + const pid = parseInt(pidContent.toString(), 10); + try { + // Check if the process is still alive, delete the PID file if not and continue waiting. + process.kill(pid, 0); + } catch { + await fs.delete(pidFile); + continue; + } + return pidContent.toString(); + } catch { + // File doesn't exist yet, wait and try again + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + } + ); +} + +/** Restarts the extension host when extension file changes are detected. Useful for development.. */ +function restartOnExtensionFileChanges(context: vscode.ExtensionContext): void { + const watcher = vscode.workspace.createFileSystemWatcher( + new vscode.RelativePattern(context.extensionPath, "dist/*.js") + ); + + context.subscriptions.push(watcher); + watcher.onDidChange(({ fsPath }) => { + vscode.window.showInformationMessage(`${fsPath.split(context.extensionPath, 2)[1]} changed. Reloading Extension Host...`); + vscode.commands.executeCommand("workbench.action.restartExtensionHost"); + }); +} + export async function deactivate(): Promise { // Clean up all extension features for (const commandRegistration of commandRegistrations) { diff --git a/src/process.ts b/src/process.ts index 052da2b8bf..f99b0cfa1b 100644 --- a/src/process.ts +++ b/src/process.ts @@ -23,6 +23,7 @@ export class PowerShellProcess { private consoleCloseSubscription?: vscode.Disposable; private pid?: number; + private pidUpdateEmitter?: vscode.EventEmitter; constructor( public exePath: string, @@ -33,10 +34,13 @@ export class PowerShellProcess { private logDirectoryPath: vscode.Uri, private startPsesArgs: string, private sessionFilePath: vscode.Uri, - private sessionSettings: Settings) { + private sessionSettings: Settings, + private devMode = false + ) { this.onExitedEmitter = new vscode.EventEmitter(); this.onExited = this.onExitedEmitter.event; + this.pidUpdateEmitter = new vscode.EventEmitter(); } public async start(cancellationToken: vscode.CancellationToken): Promise { @@ -103,7 +107,7 @@ export class PowerShellProcess { // When VS Code shell integration is enabled, the script expects certain // variables to be added to the environment. - let envMixin = undefined; + let envMixin = {}; if (this.shellIntegrationEnabled) { envMixin = { "VSCODE_INJECTION": "1", @@ -115,6 +119,12 @@ export class PowerShellProcess { }; } + // Enables Hot Reload in .NET for the attached process + // https://devblogs.microsoft.com/devops/net-enc-support-for-lambdas-and-other-improvements-in-visual-studio-2015/ + if (this.devMode) { + (envMixin as Record).COMPLUS_FORCEENC = "1"; + } + // Launch PowerShell in the integrated terminal const terminalOptions: vscode.TerminalOptions = { name: this.isTemp ? `${PowerShellProcess.title} (TEMP)` : PowerShellProcess.title, @@ -136,6 +146,7 @@ export class PowerShellProcess { this.consoleTerminal = vscode.window.createTerminal(terminalOptions); this.pid = await this.getPid(); this.logger.write(`PowerShell process started with PID: ${this.pid}`); + this.pidUpdateEmitter?.fire(this.pid); if (this.sessionSettings.integratedConsole.showOnStartup && !this.sessionSettings.integratedConsole.startInBackground) { @@ -186,6 +197,9 @@ export class PowerShellProcess { this.consoleCloseSubscription?.dispose(); this.consoleCloseSubscription = undefined; + + this.pidUpdateEmitter?.dispose(); + this.pidUpdateEmitter = undefined; } public sendKeyPress(): void { diff --git a/src/session.ts b/src/session.ts index 0b1037a116..34794a5d3e 100644 --- a/src/session.ts +++ b/src/session.ts @@ -240,6 +240,8 @@ export class SessionManager implements Middleware { this.logger.write(`Started PowerShell v${this.versionDetails.version}.`); this.setSessionRunningStatus(); // Yay, we made it! + await this.writePidIfInDevMode(this.languageServerProcess); + // Fire and forget the updater. const updater = new UpdatePowerShell(this.sessionSettings, this.logger, this.versionDetails); void updater.checkForUpdate(); @@ -303,6 +305,26 @@ export class SessionManager implements Middleware { await this.start(); } + /** In Development mode, write the PID to a file where the parent session can find it, to attach the dotnet debugger. */ + private async writePidIfInDevMode(pwshProcess: PowerShellProcess): Promise { + if (this.extensionContext.extensionMode !== vscode.ExtensionMode.Development) { return; } + const parentSessionId = process.env.VSCODE_PARENT_SESSION_ID; + const pidFilePath = vscode.Uri.joinPath(this.sessionsFolder, `PSES-${parentSessionId}.pid`); + + if (parentSessionId === undefined) { return; } + + const fs = vscode.workspace.fs; + const pid = (await pwshProcess.getPid())!.toString(); + await fs.writeFile(pidFilePath, Buffer.from(pid)); + const deletePidOnExit = pwshProcess.onExited(() => { + deletePidOnExit.dispose(); + fs.delete(pidFilePath, {useTrash: false}); + console.log(`Deleted PID file: ${pidFilePath}`); + }); + this.registeredCommands.push(deletePidOnExit); + this.extensionContext.subscriptions.push(deletePidOnExit); + } + public getSessionDetails(): IEditorServicesSessionDetails | undefined { // This is used by the debugger which should have already called `start`. if (this.sessionDetails === undefined) { @@ -569,7 +591,8 @@ export class SessionManager implements Middleware { this.extensionContext.logUri, this.getEditorServicesArgs(bundledModulesPath, powerShellExeDetails), this.getNewSessionFilePath(), - this.sessionSettings); + this.sessionSettings, + this.extensionContext.extensionMode == vscode.ExtensionMode.Development); languageServerProcess.onExited( () => {