diff --git a/.vscodeignore b/.vscodeignore index b1f9ecfada..72116c06e2 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -8,6 +8,9 @@ src/** .gitignore .travis.yml tsconfig.json +ISSUE_TEMPLATE +tslint.json +gulpfile.js **/.nyc_output/** **/coverage/** diff --git a/CHANGELOG.md b/CHANGELOG.md index bc9f93c9da..0caa43f413 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,51 @@ * When opening a .csproj-based .NET Core project in VS Code, the C# extension will not activate until a C# file is opened in the editor. ([#1150](https://github.com/OmniSharp/omnisharp-vscode/issues/1150)) * There currently is no completion support for package references in csproj files. ([#1156](https://github.com/OmniSharp/omnisharp-vscode/issues/1156)) +## 1.10.0 _(Not Yet Released)_ + +#### Completion List + +* Several improvements to the completion list! _(All contributed by [@filipw](https://github.com/filipw) with PR [omnisharp-roslyn#840](https://github.com/OmniSharp/omnisharp-roslyn/pull/840))_ + * Attributes are completed properly without the 'Attribute' suffix. ([#393](https://github.com/OmniSharp/omnisharp-vscode/issues/393)) + * Named parameters now appear in the completion list. ([#652](https://github.com/OmniSharp/omnisharp-vscode/issues/652)) + * Object initializer members now appear in the completion list. ([#260](https://github.com/OmniSharp/omnisharp-vscode/issues/260)) + * Completion appears within XML doc comment CREFs. + * Initial support for completion on 'override' and 'partial' keywords. ([#1044](https://github.com/OmniSharp/omnisharp-vscode/issues/1044)) +* New VS Code completion item glyphs (e.g. struct, event, etc.) are supported. (PR: [#1472](https://github.com/OmniSharp/omnisharp-vscode/pull/1472)) _(Contributed by [@dopare](https://github.com/dopare))_ + +#### Project System + +* Project references to projects located outside of the current workspace directory are now loaded and processed. ([#963](https://github.com/OmniSharp/omnisharp-vscode/issues/963), PR: [omnisharp-roslyn#832](https://github.com/OmniSharp/omnisharp-vscode/issues/963)) +* OmniSharp now loads .NET Core projects using the SDKs included with the .NET Core SDK appropriate for that project, if they're installed on the machine. ([#1438](https://github.com/OmniSharp/omnisharp-vscode/issues/1438), [omnisharp-roslyn#765](https://github.com/OmniSharp/omnisharp-roslyn/issues/765), PR: [omnisharp-roslyn#847](https://github.com/OmniSharp/omnisharp-roslyn/pull/847)) +* Options can now be set in an omnisharp.json to specify the Configuration (e.g. Debug) and Platform (e.g. AnyCPU) that MSBuild should use. ([omnisharp-roslyn#202](https://github.com/OmniSharp/omnisharp-roslyn/issues/202), PR: [omnisharp-roslyn#858](https://github.com/OmniSharp/omnisharp-roslyn/pull/858)) _(Contributed by [@nanoant](https://github.com/nanoant))_ +* Fixed issue where a package reference would be reported as an unresolved dependency if the reference differed from the intended dependency by case (PR: [#861](https://github.com/OmniSharp/omnisharp-roslyn/pull/861)) +* Cleaned up unresolved dependency detection in OmniSharp and added logging to help diagnose project issues. ([#1272](https://github.com/OmniSharp/omnisharp-vscode/issues/1272), PR: [#861](https://github.com/OmniSharp/omnisharp-roslyn/pull/862)) + +#### Scripting + +* Support Metadata as Source for Go To Definition in CSX files. ([omnisharp-roslyn#755](https://github.com/OmniSharp/omnisharp-roslyn/issues/755), PR: ([omnisharp-roslyn#829](https://github.com/OmniSharp/omnisharp-roslyn/pull/829)) _(Contributed by [@filipw](https://github.com/filipw))_ + +#### Unit Testing + +* MSTest support added ([#1482](https://github.com/OmniSharp/omnisharp-vscode/issues/1482), PRs: [#1478](https://github.com/OmniSharp/omnisharp-vscode/pull/1478), [omnisharp-roslyn#856](https://github.com/OmniSharp/omnisharp-roslyn/pull/856)) _(Contributed by [@AbhitejJohn](https://github.com/AbhitejJohn))_ +* Add support for NUnit Test Adapter. ([#1434](https://github.com/OmniSharp/omnisharp-vscode/issues/1434), PR: [omnisharp-roslyn#834](https://github.com/OmniSharp/omnisharp-roslyn/pull/834)) +* Tests that define display names are now run properly. ([#1426](https://github.com/OmniSharp/omnisharp-vscode/issues/1426), PR: [omnisharp-roslyn#833](https://github.com/OmniSharp/omnisharp-roslyn/pull/833)) +* Tests with similar names are no longer incorrectly run together when one of them is clicked. ([#1432](https://github.com/OmniSharp/omnisharp-vscode/issues/1432), PR: [omnisharp-roslyn#833](https://github.com/OmniSharp/omnisharp-roslyn/pull/833)) +* Improve response from running/debugging tests to include output from build and test summary. ([#419](https://github.com/OmniSharp/omnisharp-vscode/issues/419), [#455](https://github.com/OmniSharp/omnisharp-vscode/issues/455), PRs: [#1436](https://github.com/OmniSharp/omnisharp-vscode/pull/1436), [#1486](https://github.com/OmniSharp/omnisharp-vscode/pull/1486)) +* Added `csharp.unitTestDebugingOptions` setting to pass launch.json-style debug options (example: `justMyCode`) when unit test debugging. + +#### Debugger +* Resolves crash on OSX when the target process loads an embedded PDB ([#1456](https://github.com/OmniSharp/omnisharp-vscode/issues/1456)). This commonly affects users trying to debug XUnit tests. +* Enhanced exception peek display to provide additional exception properties. + +#### Other Updates and Fixes + +* New `csharp.suppressHiddenDiagnostics` setting that can be set to true to display hidden diagnostics, such as 'unnecessary using directive'. ([#1429](https://github.com/OmniSharp/omnisharp-vscode/issues/1429), PR: [#1435](https://github.com/OmniSharp/omnisharp-vscode/pull/1435)) _(Contributed by [@cruz82](https://github.com/cruz82))_ +* Fix issue causing several code snippets to not be available. ([#1459](https://github.com/OmniSharp/omnisharp-vscode/issues/1459), PR: [#1461](https://github.com/OmniSharp/omnisharp-vscode/pull/1461)) _(Contributed by [@shaunluttin](https://github.com/shaunluttin))_ +* Ensure the 'Generate Assets for Build and Debug' command can cause the extension to activate. (PR: [#1470](https://github.com/OmniSharp/omnisharp-vscode/pull/1470)) +* The OmniSharp process is now correctly terminated on Unix when the 'Restart OmniSharp' command is invoked. ([#1445](https://github.com/OmniSharp/omnisharp-vscode/issues/1445), PR: [#1466](https://github.com/OmniSharp/omnisharp-vscode/pull/1466)) +* Added new `RoslynExtensions` option to allow specifying a set of assemblies in an omnisharp.json file that OmniSharp will look in to find Roslyn extensions to load. (PR: [omnisharp-roslyn#848](https://github.com/OmniSharp/omnisharp-roslyn/pull/848)) _(Contributed by [@filipw](https://github.com/filipw))_ + ## 1.9.0 (April 20, 2017) #### Unit Testing diff --git a/package.json b/package.json index 7b1da0a4ff..b9fc3c5014 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "csharp", "publisher": "ms-vscode", - "version": "1.9.0", + "version": "1.10.0-beta5", "description": "C# for Visual Studio Code (powered by OmniSharp).", "displayName": "C#", "author": "Microsoft Corporation", @@ -123,7 +123,7 @@ }, { "description": "OmniSharp (.NET 4.6 / x86)", - "url": "https://omnisharpdownload.blob.core.windows.net/ext/omnisharp-win-x86-1.14.0.2.zip", + "url": "https://omnisharpdownload.blob.core.windows.net/ext/omnisharp-win-x86-1.19.0.zip", "installPath": "./bin/omnisharp", "platforms": [ "win32" @@ -135,7 +135,7 @@ }, { "description": "OmniSharp (.NET 4.6 / x64)", - "url": "https://omnisharpdownload.blob.core.windows.net/ext/omnisharp-win-x64-1.14.0.2.zip", + "url": "https://omnisharpdownload.blob.core.windows.net/ext/omnisharp-win-x64-1.19.0.zip", "installPath": "./bin/omnisharp", "platforms": [ "win32" @@ -147,7 +147,7 @@ }, { "description": "OmniSharp (Mono 4.6)", - "url": "https://omnisharpdownload.blob.core.windows.net/ext/omnisharp-mono-1.14.0.2.zip", + "url": "https://omnisharpdownload.blob.core.windows.net/ext/omnisharp-mono-1.19.0.zip", "installPath": "./bin/omnisharp", "platforms": [ "darwin", @@ -157,8 +157,8 @@ }, { "description": ".NET Core Debugger (Windows / x64)", - "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-9-2/coreclr-debug-win7-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-9-2/coreclr-debug-win7-x64.zip", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-10-1/coreclr-debug-win7-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-10-1/coreclr-debug-win7-x64.zip", "installPath": ".debugger", "runtimeIds": [ "win7-x64" @@ -167,8 +167,8 @@ }, { "description": ".NET Core Debugger (macOS / x64)", - "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-9-2/coreclr-debug-osx.10.11-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-9-2/coreclr-debug-osx.10.11-x64.zip", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-10-1/coreclr-debug-osx.10.11-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-10-1/coreclr-debug-osx.10.11-x64.zip", "installPath": ".debugger", "runtimeIds": [ "osx.10.11-x64" @@ -181,8 +181,8 @@ }, { "description": ".NET Core Debugger (CentOS / x64)", - "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-9-2/coreclr-debug-centos.7-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-9-2/coreclr-debug-centos.7-x64.zip", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-10-1/coreclr-debug-centos.7-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-10-1/coreclr-debug-centos.7-x64.zip", "installPath": ".debugger", "runtimeIds": [ "centos.7-x64" @@ -195,8 +195,8 @@ }, { "description": ".NET Core Debugger (Debian / x64)", - "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-9-2/coreclr-debug-debian.8-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-9-2/coreclr-debug-debian.8-x64.zip", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-10-1/coreclr-debug-debian.8-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-10-1/coreclr-debug-debian.8-x64.zip", "installPath": ".debugger", "runtimeIds": [ "debian.8-x64" @@ -209,8 +209,8 @@ }, { "description": ".NET Core Debugger (Fedora 23 / x64)", - "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-9-2/coreclr-debug-fedora.23-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-9-2/coreclr-debug-fedora.23-x64.zip", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-10-1/coreclr-debug-fedora.23-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-10-1/coreclr-debug-fedora.23-x64.zip", "installPath": ".debugger", "runtimeIds": [ "fedora.23-x64" @@ -223,8 +223,8 @@ }, { "description": ".NET Core Debugger (Fedora 24 / x64)", - "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-9-2/coreclr-debug-fedora.24-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-9-2/coreclr-debug-fedora.24-x64.zip", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-10-1/coreclr-debug-fedora.24-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-10-1/coreclr-debug-fedora.24-x64.zip", "installPath": ".debugger", "runtimeIds": [ "fedora.24-x64" @@ -237,8 +237,8 @@ }, { "description": ".NET Core Debugger (OpenSUSE 13 / x64)", - "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-9-2/coreclr-debug-opensuse.13.2-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-9-2/coreclr-debug-opensuse.13.2-x64.zip", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-10-1/coreclr-debug-opensuse.13.2-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-10-1/coreclr-debug-opensuse.13.2-x64.zip", "installPath": ".debugger", "runtimeIds": [ "opensuse.13.2-x64" @@ -251,8 +251,8 @@ }, { "description": ".NET Core Debugger (OpenSUSE 42 / x64)", - "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-9-2/coreclr-debug-opensuse.42.1-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-9-2/coreclr-debug-opensuse.42.1-x64.zip", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-10-1/coreclr-debug-opensuse.42.1-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-10-1/coreclr-debug-opensuse.42.1-x64.zip", "installPath": ".debugger", "runtimeIds": [ "opensuse.42.1-x64" @@ -265,8 +265,8 @@ }, { "description": ".NET Core Debugger (RHEL / x64)", - "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-9-2/coreclr-debug-rhel.7.2-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-9-2/coreclr-debug-rhel.7.2-x64.zip", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-10-1/coreclr-debug-rhel.7.2-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-10-1/coreclr-debug-rhel.7.2-x64.zip", "installPath": ".debugger", "runtimeIds": [ "rhel.7-x64" @@ -279,8 +279,8 @@ }, { "description": ".NET Core Debugger (Ubuntu 14.04 / x64)", - "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-9-2/coreclr-debug-ubuntu.14.04-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-9-2/coreclr-debug-ubuntu.14.04-x64.zip", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-10-1/coreclr-debug-ubuntu.14.04-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-10-1/coreclr-debug-ubuntu.14.04-x64.zip", "installPath": ".debugger", "runtimeIds": [ "ubuntu.14.04-x64" @@ -293,8 +293,8 @@ }, { "description": ".NET Core Debugger (Ubuntu 16.04 / x64)", - "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-9-2/coreclr-debug-ubuntu.16.04-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-9-2/coreclr-debug-ubuntu.16.04-x64.zip", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-10-1/coreclr-debug-ubuntu.16.04-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-10-1/coreclr-debug-ubuntu.16.04-x64.zip", "installPath": ".debugger", "runtimeIds": [ "ubuntu.16.04-x64" @@ -307,8 +307,8 @@ }, { "description": ".NET Core Debugger (Ubuntu 16.10 / x64)", - "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-9-2/coreclr-debug-ubuntu.16.10-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-9-2/coreclr-debug-ubuntu.16.10-x64.zip", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-10-1/coreclr-debug-ubuntu.16.10-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-10-1/coreclr-debug-ubuntu.16.10-x64.zip", "installPath": ".debugger", "runtimeIds": [ "ubuntu.16.10-x64" @@ -321,7 +321,7 @@ } ], "engines": { - "vscode": "^1.10.1" + "vscode": "^1.12.0" }, "activationEvents": [ "onLanguage:csharp", @@ -329,6 +329,7 @@ "onCommand:o.pickProjectAndStart", "onCommand:o.showOutput", "onCommand:dotnet.restore", + "onCommand:dotnet.generateAssets", "onCommand:csharp.downloadDebugger", "onCommand:csharp.listProcess", "onCommand:csharp.listRemoteProcess", @@ -359,11 +360,94 @@ ], "description": "If the current Linux distribution is not recognized, this option can be used to tell the debugger what version can be used. After changing this option, close VS Code, remove the debugger folder (~/.vscode/extensions/ms-vscode.csharp-/.debugger) if it has already downloaded, and restart VS Code." }, + "csharp.unitTestDebugingOptions": { + "type": "object", + "description": "Options to use with the debugger when launching for unit test debugging. Any launch.json option is valid here.", + "default": {}, + "properties": { + "sourceFileMap": { + "type": "object", + "description": "Optional source file mappings passed to the debug engine. Example: '{ \"C:\\foo\":\"/home/user/foo\" }'", + "additionalProperties": { + "type": "string" + }, + "default": { + "": "" + } + }, + "justMyCode": { + "type": "boolean", + "description": "Optional flag to only show user code.", + "default": true + }, + "symbolPath": { + "type": "array", + "description": "Array of directories to use to search for .pdb files. These directories will be searched in addition to the default locations -- next to the module and the path where the pdb was originally dropped to. Example: '[ \"/Volumes/symbols\" ]", + "items": { + "type": "string" + }, + "default": [] + }, + "requireExactSource": { + "type": "boolean", + "description": "Optional flag to require current source code to match the pdb.", + "default": true + }, + "enableStepFiltering": { + "type": "boolean", + "description": "Optional flag to enable stepping over Properties and Operators.", + "default": true + }, + "debugServer": { + "type": "number", + "description": "For debug extension development only: if a port is specified VS Code tries to connect to a debug adapter running in server mode", + "default": 4711 + }, + "logging": { + "description": "Optional flags to determine what types of messages should be logged to the output window.", + "type": "object", + "required": [], + "default": {}, + "properties": { + "exceptions": { + "type": "boolean", + "description": "Optional flag to determine whether exception messages should be logged to the output window.", + "default": true + }, + "moduleLoad": { + "type": "boolean", + "description": "Optional flag to determine whether module load events should be logged to the output window.", + "default": true + }, + "programOutput": { + "type": "boolean", + "description": "Optional flag to determine whether program output should be logged to the output window when not using an external console.", + "default": true + }, + "engineLogging": { + "type": "boolean", + "description": "Optional flag to determine whether diagnostic engine logs should be logged to the output window.", + "default": false + }, + "browserStdOut": { + "type": "boolean", + "description": "Optional flag to determine if stdout text from the launching the web browser should be logged to the output window.", + "default": true + } + } + } + } + }, "csharp.suppressDotnetRestoreNotification": { "type": "boolean", "default": false, "description": "Suppress the notification window to perform a 'dotnet restore' when dependencies can't be resolved." }, + "csharp.suppressHiddenDiagnostics": { + "type": "boolean", + "default": true, + "description": "Suppress 'hidden' diagnostics (such as 'unnecessary using directives') from appearing in the editor or the Problems pane." + }, "omnisharp.path": { "type": [ "string", @@ -469,21 +553,6 @@ "command": "o.showOutput", "key": "Ctrl+L L", "mac": "Cmd+L L" - }, - { - "key": "shift+0", - "command": "^acceptSelectedSuggestion", - "when": "editorTextFocus && suggestWidgetVisible && editorLangId == 'csharp' && suggestionSupportsAcceptOnKey" - }, - { - "key": "shift+9", - "command": "^acceptSelectedSuggestion", - "when": "editorTextFocus && suggestWidgetVisible && editorLangId == 'csharp' && suggestionSupportsAcceptOnKey" - }, - { - "key": ".", - "command": "^acceptSelectedSuggestion", - "when": "editorTextFocus && suggestWidgetVisible && editorLangId == 'csharp' && suggestionSupportsAcceptOnKey" } ], "snippets": [ @@ -514,8 +583,7 @@ "launch": { "type": "object", "required": [ - "program", - "cwd" + "program" ], "properties": { "program": { @@ -529,12 +597,21 @@ "default": "${workspaceRoot}" }, "args": { - "type": "array", - "description": "Command line arguments passed to the program.", - "items": { - "type": "string" - }, - "default": [] + "anyOf": [ + { + "type": "array", + "description": "Command line arguments passed to the program.", + "items": { + "type": "string" + }, + "default": [] + }, + { + "type": "string", + "description": "Stringified version of command line arguments passed to the program.", + "default": "" + } + ] }, "stopAtEntry": { "type": "boolean", @@ -565,8 +642,21 @@ "default": true }, "args": { - "type": "string", - "description": "The arguments to pass to the command to open the browser. Use ${auto-detect-url} to automatically use the address the server is listening to", + "anyOf": [ + { + "type": "array", + "description": "Command line arguments passed to the program.", + "items": { + "type": "string" + }, + "default": [] + }, + { + "type": "string", + "description": "Stringified version of command line arguments passed to the program.", + "default": "" + } + ], "default": "${auto-detect-url}" }, "osx": { @@ -747,11 +837,21 @@ "default": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'" }, "pipeArgs": { - "type": "array", - "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", - "items": { - "type": "string" - }, + "anyOf": [ + { + "type": "array", + "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "items": { + "type": "string" + }, + "default": [] + }, + { + "type": "string", + "description": "Stringified version of command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "default": "" + } + ], "default": [] }, "debuggerPath": { @@ -792,11 +892,21 @@ "default": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'" }, "pipeArgs": { - "type": "array", - "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", - "items": { - "type": "string" - }, + "anyOf": [ + { + "type": "array", + "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "items": { + "type": "string" + }, + "default": [] + }, + { + "type": "string", + "description": "Stringified version of command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "default": "" + } + ], "default": [] }, "quoteArgs": { @@ -834,11 +944,21 @@ "default": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'" }, "pipeArgs": { - "type": "array", - "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", - "items": { - "type": "string" - }, + "anyOf": [ + { + "type": "array", + "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "items": { + "type": "string" + }, + "default": [] + }, + { + "type": "string", + "description": "Stringified version of command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "default": "" + } + ], "default": [] }, "quoteArgs": { @@ -876,11 +996,21 @@ "default": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'" }, "pipeArgs": { - "type": "array", - "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", - "items": { - "type": "string" - }, + "anyOf": [ + { + "type": "array", + "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "items": { + "type": "string" + }, + "default": [] + }, + { + "type": "string", + "description": "Stringified version of command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "default": "" + } + ], "default": [] }, "quoteArgs": { @@ -1015,11 +1145,21 @@ "default": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'" }, "pipeArgs": { - "type": "array", - "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", - "items": { - "type": "string" - }, + "anyOf": [ + { + "type": "array", + "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "items": { + "type": "string" + }, + "default": [] + }, + { + "type": "string", + "description": "Stringified version of command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "default": "" + } + ], "default": [] }, "debuggerPath": { @@ -1060,11 +1200,21 @@ "default": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'" }, "pipeArgs": { - "type": "array", - "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", - "items": { - "type": "string" - }, + "anyOf": [ + { + "type": "array", + "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "items": { + "type": "string" + }, + "default": [] + }, + { + "type": "string", + "description": "Stringified version of command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "default": "" + } + ], "default": [] }, "quoteArgs": { @@ -1102,11 +1252,21 @@ "default": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'" }, "pipeArgs": { - "type": "array", - "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", - "items": { - "type": "string" - }, + "anyOf": [ + { + "type": "array", + "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "items": { + "type": "string" + }, + "default": [] + }, + { + "type": "string", + "description": "Stringified version of command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "default": "" + } + ], "default": [] }, "quoteArgs": { @@ -1144,11 +1304,21 @@ "default": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'" }, "pipeArgs": { - "type": "array", - "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", - "items": { - "type": "string" - }, + "anyOf": [ + { + "type": "array", + "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "items": { + "type": "string" + }, + "default": [] + }, + { + "type": "string", + "description": "Stringified version of command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "default": "" + } + ], "default": [] }, "quoteArgs": { @@ -1340,8 +1510,7 @@ "launch": { "type": "object", "required": [ - "program", - "cwd" + "program" ], "properties": { "program": { @@ -1355,12 +1524,21 @@ "default": "${workspaceRoot}" }, "args": { - "type": "array", - "description": "Command line arguments passed to the program.", - "items": { - "type": "string" - }, - "default": [] + "anyOf": [ + { + "type": "array", + "description": "Command line arguments passed to the program.", + "items": { + "type": "string" + }, + "default": [] + }, + { + "type": "string", + "description": "Stringified version of command line arguments passed to the program.", + "default": "" + } + ] }, "stopAtEntry": { "type": "boolean", @@ -1391,8 +1569,21 @@ "default": true }, "args": { - "type": "string", - "description": "The arguments to pass to the command to open the browser. Use ${auto-detect-url} to automatically use the address the server is listening to", + "anyOf": [ + { + "type": "array", + "description": "Command line arguments passed to the program.", + "items": { + "type": "string" + }, + "default": [] + }, + { + "type": "string", + "description": "Stringified version of command line arguments passed to the program.", + "default": "" + } + ], "default": "${auto-detect-url}" }, "osx": { @@ -1573,11 +1764,21 @@ "default": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'" }, "pipeArgs": { - "type": "array", - "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", - "items": { - "type": "string" - }, + "anyOf": [ + { + "type": "array", + "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "items": { + "type": "string" + }, + "default": [] + }, + { + "type": "string", + "description": "Stringified version of command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "default": "" + } + ], "default": [] }, "debuggerPath": { @@ -1618,11 +1819,21 @@ "default": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'" }, "pipeArgs": { - "type": "array", - "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", - "items": { - "type": "string" - }, + "anyOf": [ + { + "type": "array", + "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "items": { + "type": "string" + }, + "default": [] + }, + { + "type": "string", + "description": "Stringified version of command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "default": "" + } + ], "default": [] }, "quoteArgs": { @@ -1660,11 +1871,21 @@ "default": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'" }, "pipeArgs": { - "type": "array", - "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", - "items": { - "type": "string" - }, + "anyOf": [ + { + "type": "array", + "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "items": { + "type": "string" + }, + "default": [] + }, + { + "type": "string", + "description": "Stringified version of command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "default": "" + } + ], "default": [] }, "quoteArgs": { @@ -1702,11 +1923,21 @@ "default": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'" }, "pipeArgs": { - "type": "array", - "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", - "items": { - "type": "string" - }, + "anyOf": [ + { + "type": "array", + "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "items": { + "type": "string" + }, + "default": [] + }, + { + "type": "string", + "description": "Stringified version of command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "default": "" + } + ], "default": [] }, "quoteArgs": { @@ -1841,11 +2072,21 @@ "default": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'" }, "pipeArgs": { - "type": "array", - "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", - "items": { - "type": "string" - }, + "anyOf": [ + { + "type": "array", + "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "items": { + "type": "string" + }, + "default": [] + }, + { + "type": "string", + "description": "Stringified version of command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "default": "" + } + ], "default": [] }, "debuggerPath": { @@ -1886,11 +2127,21 @@ "default": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'" }, "pipeArgs": { - "type": "array", - "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", - "items": { - "type": "string" - }, + "anyOf": [ + { + "type": "array", + "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "items": { + "type": "string" + }, + "default": [] + }, + { + "type": "string", + "description": "Stringified version of command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "default": "" + } + ], "default": [] }, "quoteArgs": { @@ -1928,11 +2179,21 @@ "default": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'" }, "pipeArgs": { - "type": "array", - "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", - "items": { - "type": "string" - }, + "anyOf": [ + { + "type": "array", + "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "items": { + "type": "string" + }, + "default": [] + }, + { + "type": "string", + "description": "Stringified version of command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "default": "" + } + ], "default": [] }, "quoteArgs": { @@ -1970,11 +2231,21 @@ "default": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'" }, "pipeArgs": { - "type": "array", - "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", - "items": { - "type": "string" - }, + "anyOf": [ + { + "type": "array", + "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "items": { + "type": "string" + }, + "default": [] + }, + { + "type": "string", + "description": "Stringified version of command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "default": "" + } + ], "default": [] }, "quoteArgs": { diff --git a/snippets/csharp.json b/snippets/csharp.json index 9f31803fe0..92fb47652f 100644 --- a/snippets/csharp.json +++ b/snippets/csharp.json @@ -1,553 +1,553 @@ { - "Attribute using recommended pattern": { - - "prefix": "attribute", - "body": [ - "[System.AttributeUsage(System.AttributeTargets.${All}, Inherited = ${false}, AllowMultiple = ${true})]", - "sealed class ${My}Attribute : System.Attribute", - "{", - " // See the attribute guidelines at", - " // http://go.microsoft.com/fwlink/?LinkId=85236", - " readonly string positionalString;", - " ", - " // This is a positional argument", - " public ${My}Attribute (string positionalString)", - " {", - " this.positionalString = positionalString;", - " ", - " // TODO: Implement code here", - " ${throw new System.NotImplementedException();}", - " }", - " ", - " public string PositionalString", - " {", - " get { return positionalString; }", - " }", - " ", - " // This is a named argument", - " public int NamedInt { get; set; }", - "}" - ], - "description": "Attribute using recommended pattern" - }, - - "Checked block": { - - "prefix": "checked", - "body": [ - "checked", - "{", - " $0", - "}" - ], - "description": "Checked block" - }, - - "Class": { - - "prefix": "class", - "body": [ - "class ${Name}", - "{", - " $0", - "}" - ], - "description": "Class" - }, - - "Console.WriteLine": { - - "prefix": "cw", - "body": [ - "System.Console.WriteLine($0);" - ], - "description": "Console.WriteLine" - }, - - "do...while loop": { - - "prefix": "do", - "body": [ - "do", - "{", - " $0", - "} while (${true});" - ], - "description": "do...while loop" - }, - - "Else statement": { - - "prefix": "else", - "body": [ - "else", - "{", - " $0", - "}" - ], - "description": "Else statement" - }, - - "Enum": { - - "prefix": "enum", - "body": [ - "enum ${Name}", - "{", - " $0", - "}" - ], - "description": "Enum" - }, - - "Implementing Equals() according to guidelines": { - - "prefix": "equals", - "body": [ - "// override object.Equals", - "public override bool Equals (object obj)", - "{", - " //", - " // See the full list of guidelines at", - " // http://go.microsoft.com/fwlink/?LinkID=85237", - " // and also the guidance for operator== at", - " // http://go.microsoft.com/fwlink/?LinkId=85238", - " //", - " ", - " if (obj == null || GetType() != obj.GetType())", - " {", - " return false;", - " }", - " ", - " // TODO: write your implementation of Equals() here", - " ${1:throw new System.NotImplementedException();}", - " return base.Equals (obj);", - "}", - "", - "// override object.GetHashCode", - "public override int GetHashCode()", - "{", - " // TODO: write your implementation of GetHashCode() here", - " ${2:throw new System.NotImplementedException();}", - " return base.GetHashCode();", - "}" - ], - "description": "Implementing Equals() according to guidelines" - }, - - "Exception": { - - "prefix": "exception", - "body": [ - "[System.Serializable]", - "public class ${My}Exception : ${System.Exception}", - "{", - " public ${My}Exception() { }", - " public ${My}Exception( string message ) : base( message ) { }", - " public ${My}Exception( string message, System.Exception inner ) : base( message, inner ) { }", - " protected ${My}Exception(", - " System.Runtime.Serialization.SerializationInfo info,", - " System.Runtime.Serialization.StreamingContext context ) : base( info, context ) { }", - "}" - ], - "description": "Exception" - }, - - "Foreach statement": { - - "prefix": "foreach", - "body": [ - "foreach (${var} ${item} in ${collection})", - "{", - " $0", - "}" - ], - "description": "Foreach statement" - }, - - "Reverse for loop": { - - "prefix": "forr", - "body": [ - "for (int ${i} = ${length} - 1; ${i} >= 0 ; ${i}--)", - "{", - " $0", - "}" - ], - "description": "Reverse for loop" - }, - - "for loop": { - - "prefix": "for", - "body": [ - "for (int ${i} = 0; ${i} < ${length}; ${i}++)", - "{", - " $0", - "}" - ], - "description": "for loop" - }, - - "if statement": { - - "prefix": "if", - "body": [ - "if (${true})", - "{", - " $0", - "}" - ], - "description": "if statement" - }, - - "Indexer": { - - "prefix": "indexer", - "body": [ - "${public} ${object} this[${int} index]", - "{", - " get { $0 }", - " set { $1 }", - "}" - ], - "description": "Indexer" - }, - - "Interface": { - - "prefix": "interface", - "body": [ - "interface I${Name}", - "{", - " $0", - "}" - ], - "description": "Interface" - }, - - "Safely invoking an event": { - - "prefix": "invoke", - "body": [ - "${EventHandler} temp = ${MyEvent};", - "if (temp != null)", - "{", - " temp($0);", - "}" - ], - "description": "Safely invoking an event" - }, - - "Simple iterator": { - - "prefix": "iterator", - "body": [ - "public System.Collections.Generic.IEnumerator<${ElementType}> GetEnumerator()", - "{", - " $0throw new System.NotImplementedException();", - " yield return default(${ElementType});", - "}" - ], - "description": "Simple iterator" - }, - - "Named iterator/indexer pair using a nested class": { - - "prefix": "iterindex", - "body": [ - "public ${Name}Iterator ${Name}", - "{", - " get", - " {", - " return new ${Name}Iterator(this);", - " }", - "}", - "", - "public class ${Name}Iterator", - "{", - " readonly ${ClassName} outer;", - " ", - " internal ${Name}Iterator(${ClassName} outer)", - " {", - " this.outer = outer;", - " }", - " ", - " // TODO: provide an appropriate implementation here", - " public int Length { get { return 1; } }", - " ", - " public ${ElementType} this[int index]", - " {", - " get", - " {", - " //", - " // TODO: implement indexer here", - " //", - " // you have full access to ${ClassName} privates", - " //", - " ${throw new System.NotImplementedException();}", - " return default(${ElementType});", - " }", - " }", - " ", - " public System.Collections.Generic.IEnumerator<${ElementType}> GetEnumerator()", - " {", - " for (int i = 0; i < this.Length; i++)", - " {", - " yield return this[i];", - " }", - " }", - "}" - ], - "description": "Named iterator/indexer pair using a nested class" - }, - - "Lock statement": { - - "prefix": "lock", - "body": [ - "lock (${this})", - "{", - " $0", - "}" - ], - "description": "Lock statement" - }, - - "MessageBox.Show": { - - "prefix": "mbox", - "body": [ - "System.Windows.Forms.MessageBox.Show(\"${Text}\");$0" - ], - "description": "MessageBox.Show" - }, - - "Namespace": { - - "prefix": "namespace", - "body": [ - "namespace ${Name}", - "{", - " $0", - "}" - ], - "description": "Namespace" - }, - - "#if": { - - "prefix": "ifd", - "body": [ - "#if ${true}", - " $0", - "#endif" - ], - "description": "#if" - }, - - "#region": { - - "prefix": "region", - "body": [ - "#region ${Name}", - " $0", - "#endregion" - ], - "description": "#region" - }, - - "Property and backing field": { - - "prefix": "propfull", - "body": [ - "private ${int} ${myVar};", - - "public ${int} ${MyProperty}", - "{", - " get { return ${myVar};}", - " set { ${myVar} = value;}", - "}", - "$0" - ], - "description": "Property and backing field" - }, - - "propg": { - - "prefix": "propg", - "body": [ - "public ${int} ${MyProperty} { get; private set; }$0" - ], - "description": "An automatically implemented property with a 'get' accessor and a private 'set' accessor. C# 3.0 or higher" - }, - - "prop": { - - "prefix": "prop", - "body": [ - "public ${int} ${MyProperty} { get; set; }$0" - ], - "description": "An automatically implemented property. C# 3.0 or higher" - }, - - "sim": { - - "prefix": "sim", - "body": [ - "static int Main(string[] args)", - "{", - " $0", - " return 0;", - "}" - ], - "description": "int Main()" - }, - - "Struct": { - - "prefix": "struct", - "body": [ - "struct ${Name}", - "{", - " $0", - "}" - ], - "description": "Struct" - }, - - "svm": { - - "prefix": "svm", - "body": [ - "static void Main(string[] args)", - "{", - " $0", - "}" - ], - "description": "void Main()" - }, - - "Switch statement": { - - "prefix": "switch", - "body": [ - "switch (${switch_on})", - "{", - " $0", - " default:", - "}" - ], - "description": "Switch statement" - }, - - "Try finally": { - - "prefix": "tryf", - "body": [ - "try", - "{", - " ${_}", - "}", - "finally", - "{", - " $0", - "}" - ], - "description": "Try finally" - }, - - "Try catch": { - - "prefix": "try", - "body": [ - "try", - "{", - " ${_}", - "}", - "catch (${System.Exception})", - "{", - " $0", - " throw;", - "}" - ], - "description": "Try catch" - }, - - "Unchecked block": { - - "prefix": "unchecked", - "body": [ - "unchecked", - "{", - " $0", - "}" - ], - "description": "Unchecked block" - }, - - "Unsafe statement": { - - "prefix": "unsafe", - "body": [ - "unsafe", - "{", - " $0", - "}" - ], - "description": "Unsafe statement" - }, - - "Using statement": { - - "prefix": "using", - "body": [ - "using(${resource})", - "{", - " $0", - "}" - ], - "description": "Using statement" - }, - - "While loop": { - - "prefix": "while", - "body": [ - "while (${true})", - "{", - " $0", - "}" - ], - "description": "While loop" - }, - - "constructor": { - - "prefix": "ctor", - "body": [ - "${1:public} ${2:ClassName}(${3:Parameters})", - "{", - " ${0}", - "}" - ], - "description": "constructor" - }, + "Attribute using recommended pattern": { + + "prefix": "attribute", + "body": [ + "[System.AttributeUsage(System.AttributeTargets.${All}, Inherited = ${false}, AllowMultiple = ${true})]", + "sealed class ${My}Attribute : System.Attribute", + "{", + " // See the attribute guidelines at", + " // http://go.microsoft.com/fwlink/?LinkId=85236", + " readonly string positionalString;", + " ", + " // This is a positional argument", + " public ${My}Attribute (string positionalString)", + " {", + " this.positionalString = positionalString;", + " ", + " // TODO: Implement code here", + " ${throw new System.NotImplementedException();}", + " }", + " ", + " public string PositionalString", + " {", + " get { return positionalString; }", + " }", + " ", + " // This is a named argument", + " public int NamedInt { get; set; }", + "}" + ], + "description": "Attribute using recommended pattern" + }, + + "Checked block": { + + "prefix": "checked", + "body": [ + "checked", + "{", + " $0", + "}" + ], + "description": "Checked block" + }, + + "Class": { + + "prefix": "class", + "body": [ + "class ${Name}", + "{", + " $0", + "}" + ], + "description": "Class" + }, + + "Console.WriteLine": { + + "prefix": "cw", + "body": [ + "System.Console.WriteLine($0);" + ], + "description": "Console.WriteLine" + }, + + "do...while loop": { + + "prefix": "do", + "body": [ + "do", + "{", + " $0", + "} while (${true});" + ], + "description": "do...while loop" + }, + + "Else statement": { + + "prefix": "else", + "body": [ + "else", + "{", + " $0", + "}" + ], + "description": "Else statement" + }, + + "Enum": { + + "prefix": "enum", + "body": [ + "enum ${Name}", + "{", + " $0", + "}" + ], + "description": "Enum" + }, + + "Implementing Equals() according to guidelines": { + + "prefix": "equals", + "body": [ + "// override object.Equals", + "public override bool Equals (object obj)", + "{", + " //", + " // See the full list of guidelines at", + " // http://go.microsoft.com/fwlink/?LinkID=85237", + " // and also the guidance for operator== at", + " // http://go.microsoft.com/fwlink/?LinkId=85238", + " //", + " ", + " if (obj == null || GetType() != obj.GetType())", + " {", + " return false;", + " }", + " ", + " // TODO: write your implementation of Equals() here", + " ${1:throw new System.NotImplementedException();}", + " return base.Equals (obj);", + "}", + "", + "// override object.GetHashCode", + "public override int GetHashCode()", + "{", + " // TODO: write your implementation of GetHashCode() here", + " ${2:throw new System.NotImplementedException();}", + " return base.GetHashCode();", + "}" + ], + "description": "Implementing Equals() according to guidelines" + }, + + "Exception": { + + "prefix": "exception", + "body": [ + "[System.Serializable]", + "public class ${My}Exception : ${System.Exception}", + "{", + " public ${My}Exception() { }", + " public ${My}Exception( string message ) : base( message ) { }", + " public ${My}Exception( string message, System.Exception inner ) : base( message, inner ) { }", + " protected ${My}Exception(", + " System.Runtime.Serialization.SerializationInfo info,", + " System.Runtime.Serialization.StreamingContext context ) : base( info, context ) { }", + "}" + ], + "description": "Exception" + }, + + "Foreach statement": { + + "prefix": "foreach", + "body": [ + "foreach (${var} ${item} in ${collection})", + "{", + " $0", + "}" + ], + "description": "Foreach statement" + }, + + "Reverse for loop": { + + "prefix": "forr", + "body": [ + "for (int ${i} = ${length} - 1; ${i} >= 0 ; ${i}--)", + "{", + " $0", + "}" + ], + "description": "Reverse for loop" + }, + + "for loop": { + + "prefix": "for", + "body": [ + "for (int ${i} = 0; ${i} < ${length}; ${i}++)", + "{", + " $0", + "}" + ], + "description": "for loop" + }, + + "if statement": { + + "prefix": "if", + "body": [ + "if (${true})", + "{", + " $0", + "}" + ], + "description": "if statement" + }, + + "Indexer": { + + "prefix": "indexer", + "body": [ + "${public} ${object} this[${int} index]", + "{", + " get { $0 }", + " set { $1 }", + "}" + ], + "description": "Indexer" + }, + + "Interface": { + + "prefix": "interface", + "body": [ + "interface I${Name}", + "{", + " $0", + "}" + ], + "description": "Interface" + }, + + "Safely invoking an event": { + + "prefix": "invoke", + "body": [ + "${EventHandler} temp = ${MyEvent};", + "if (temp != null)", + "{", + " temp($0);", + "}" + ], + "description": "Safely invoking an event" + }, + + "Simple iterator": { + + "prefix": "iterator", + "body": [ + "public System.Collections.Generic.IEnumerator<${ElementType}> GetEnumerator()", + "{", + " $0throw new System.NotImplementedException();", + " yield return default(${ElementType});", + "}" + ], + "description": "Simple iterator" + }, + + "Named iterator/indexer pair using a nested class": { + + "prefix": "iterindex", + "body": [ + "public ${Name}Iterator ${Name}", + "{", + " get", + " {", + " return new ${Name}Iterator(this);", + " }", + "}", + "", + "public class ${Name}Iterator", + "{", + " readonly ${ClassName} outer;", + " ", + " internal ${Name}Iterator(${ClassName} outer)", + " {", + " this.outer = outer;", + " }", + " ", + " // TODO: provide an appropriate implementation here", + " public int Length { get { return 1; } }", + " ", + " public ${ElementType} this[int index]", + " {", + " get", + " {", + " //", + " // TODO: implement indexer here", + " //", + " // you have full access to ${ClassName} privates", + " //", + " ${throw new System.NotImplementedException();}", + " return default(${ElementType});", + " }", + " }", + " ", + " public System.Collections.Generic.IEnumerator<${ElementType}> GetEnumerator()", + " {", + " for (int i = 0; i < this.Length; i++)", + " {", + " yield return this[i];", + " }", + " }", + "}" + ], + "description": "Named iterator/indexer pair using a nested class" + }, + + "Lock statement": { + + "prefix": "lock", + "body": [ + "lock (${this})", + "{", + " $0", + "}" + ], + "description": "Lock statement" + }, + + "MessageBox.Show": { + + "prefix": "mbox", + "body": [ + "System.Windows.Forms.MessageBox.Show(\"${Text}\");$0" + ], + "description": "MessageBox.Show" + }, + + "Namespace": { + + "prefix": "namespace", + "body": [ + "namespace ${Name}", + "{", + " $0", + "}" + ], + "description": "Namespace" + }, + + "#if": { + + "prefix": "ifd", + "body": [ + "#if ${true}", + " $0", + "#endif" + ], + "description": "#if" + }, + + "#region": { + + "prefix": "region", + "body": [ + "#region ${Name}", + " $0", + "#endregion" + ], + "description": "#region" + }, + + "Property and backing field": { + + "prefix": "propfull", + "body": [ + "private ${int} ${myVar};", + + "public ${int} ${MyProperty}", + "{", + " get { return ${myVar};}", + " set { ${myVar} = value;}", + "}", + "$0" + ], + "description": "Property and backing field" + }, + + "propg": { + + "prefix": "propg", + "body": [ + "public ${int} ${MyProperty} { get; private set; }$0" + ], + "description": "An automatically implemented property with a 'get' accessor and a private 'set' accessor. C# 3.0 or higher" + }, + + "prop": { + + "prefix": "prop", + "body": [ + "public ${int} ${MyProperty} { get; set; }$0" + ], + "description": "An automatically implemented property. C# 3.0 or higher" + }, + + "sim": { + + "prefix": "sim", + "body": [ + "static int Main(string[] args)", + "{", + " $0", + " return 0;", + "}" + ], + "description": "int Main()" + }, + + "Struct": { + + "prefix": "struct", + "body": [ + "struct ${Name}", + "{", + " $0", + "}" + ], + "description": "Struct" + }, + + "svm": { + + "prefix": "svm", + "body": [ + "static void Main(string[] args)", + "{", + " $0", + "}" + ], + "description": "void Main()" + }, + + "Switch statement": { + + "prefix": "switch", + "body": [ + "switch (${switch_on})", + "{", + " $0", + " default:", + "}" + ], + "description": "Switch statement" + }, + + "Try finally": { + + "prefix": "tryf", + "body": [ + "try", + "{", + " ${_}", + "}", + "finally", + "{", + " $0", + "}" + ], + "description": "Try finally" + }, + + "Try catch": { + + "prefix": "try", + "body": [ + "try", + "{", + " ${_}", + "}", + "catch (${System.Exception})", + "{", + " $0", + " throw;", + "}" + ], + "description": "Try catch" + }, + + "Unchecked block": { + + "prefix": "unchecked", + "body": [ + "unchecked", + "{", + " $0", + "}" + ], + "description": "Unchecked block" + }, + + "Unsafe statement": { + + "prefix": "unsafe", + "body": [ + "unsafe", + "{", + " $0", + "}" + ], + "description": "Unsafe statement" + }, + + "Using statement": { + + "prefix": "using", + "body": [ + "using(${resource})", + "{", + " $0", + "}" + ], + "description": "Using statement" + }, + + "While loop": { + + "prefix": "while", + "body": [ + "while (${true})", + "{", + " $0", + "}" + ], + "description": "While loop" + }, + + "constructor": { + + "prefix": "ctor", + "body": [ + "${1:public} ${2:ClassName}(${3:Parameters})", + "{", + " ${0}", + "}" + ], + "description": "constructor" + }, "xUnit Test": { - "prefix": "fact", - "body": [ - "[Fact]", - "public void ${1:TestName}()", - "{", - "//Given", - "", - "//When", - "", - "//Then", - "}${0}" - ], - "description": "create xunit test method" - } + "prefix": "fact", + "body": [ + "[Fact]", + "public void ${1:TestName}()", + "{", + "//Given", + "", + "//When", + "", + "//Then", + "}${0}" + ], + "description": "create xunit test method" + } } diff --git a/src/common.ts b/src/common.ts index 58b1ced8b4..fbf5eda9c7 100644 --- a/src/common.ts +++ b/src/common.ts @@ -5,6 +5,7 @@ import * as cp from 'child_process'; import * as fs from 'fs'; +import * as os from 'os'; import * as path from 'path'; let extensionPath: string; @@ -60,6 +61,41 @@ export function execChildProcess(command: string, workingDirectory: string = get }); } +export function getUnixChildProcessIds(pid: number): Promise { + return new Promise((resolve, reject) => { + let ps = cp.exec('ps -A -o ppid,pid', (error, stdout, stderr) => + { + if (error) { + return reject(error); + } + + if (stderr) { + return reject(stderr); + } + + if (!stdout) { + return resolve([]); + } + + let lines = stdout.split(os.EOL); + let pairs = lines.map(line => line.trim().split(/\s+/)); + + let children = []; + + for (let pair of pairs) { + let ppid = parseInt(pair[0]); + if (ppid === pid) { + children.push(parseInt(pair[1])); + } + } + + resolve(children); + }); + + ps.on('error', reject); + }); +} + export function fileExists(filePath: string): Promise { return new Promise((resolve, reject) => { fs.stat(filePath, (err, stats) => { @@ -135,58 +171,4 @@ export function deleteInstallFile(type: InstallFileType): Promise { export function convertNativePathToPosix(pathString: string): string { let parts = pathString.split(path.sep); return parts.join(path.posix.sep); -} - -/** - * Splits a string of command line arguments into a string array. This function - * handles double-quoted arguments, but it is tailored toward the needs of the - * text returned by VSTest, and is not generally useful as a command line parser. - * @param commandLineString Text of command line arguments - */ -export function splitCommandLineArgs(commandLineString: string): string[] { - let result = []; - let start = -1; - let index = 0; - let inQuotes = false; - - while (index < commandLineString.length) { - let ch = commandLineString[index]; - - // Are we starting a new word? - if (start === -1 && ch !== ' ' && ch !== '"') { - start = index; - } - - // is next character quote? - if (ch === '"') { - // Are we already in a quoted argument? If so, push the argument to the result list. - // If not, start a new quoted argument. - if (inQuotes) { - let arg = start >= 0 - ? commandLineString.substring(start, index) - : ""; - result.push(arg); - start = -1; - inQuotes = false; - } - else { - inQuotes = true; - } - } - - if (!inQuotes && start >= 0 && ch === ' ') { - let arg = commandLineString.substring(start, index); - result.push(arg); - start = -1; - } - - index++; - } - - if (start >= 0) { - let arg = commandLineString.substring(start, commandLineString.length); - result.push(arg); - } - - return result; -} +} \ No newline at end of file diff --git a/src/features/completionItemProvider.ts b/src/features/completionItemProvider.ts index 855c66963d..f2ce919967 100644 --- a/src/features/completionItemProvider.ts +++ b/src/features/completionItemProvider.ts @@ -14,6 +14,12 @@ import {CompletionItemProvider, CompletionItem, CompletionItemKind, Cancellation export default class OmniSharpCompletionItemProvider extends AbstractSupport implements CompletionItemProvider { + // copied from Roslyn here: https://github.com/dotnet/roslyn/blob/6e8f6d600b6c4bc0b92bc3d782a9e0b07e1c9f8e/src/Features/Core/Portable/Completion/CompletionRules.cs#L166-L169 + private static DefaultCommitCharacters = [ + ' ', '{', '}', '[', ']', '(', ')', '.', ',', ':', + ';', '+', '-', '*', '/', '%', '&', '|', '^', '!', + '~', '=', '<', '>', '?', '@', '#', '\'', '\"', '\\']; + public provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken): Promise { let wordToComplete = ''; @@ -28,9 +34,9 @@ export default class OmniSharpCompletionItemProvider extends AbstractSupport imp req.WantKind = true; req.WantReturnType = true; - return serverUtils.autoComplete(this._server, req).then(values => { + return serverUtils.autoComplete(this._server, req).then(responses => { - if (!values) { + if (!responses) { return; } @@ -39,11 +45,17 @@ export default class OmniSharpCompletionItemProvider extends AbstractSupport imp // transform AutoCompleteResponse to CompletionItem and // group by code snippet - for (let value of values) { - let completion = new CompletionItem(value.CompletionText.replace(/\(|\)|<|>/g, '')); - completion.detail = value.ReturnType ? `${value.ReturnType} ${value.DisplayText}` : value.DisplayText; - completion.documentation = extractSummaryText(value.Description); - completion.kind = _kinds[value.Kind] || CompletionItemKind.Property; + for (let response of responses) { + let completion = new CompletionItem(response.CompletionText); + + completion.detail = response.ReturnType + ? `${response.ReturnType} ${response.DisplayText}` + : response.DisplayText; + + completion.documentation = extractSummaryText(response.Description); + completion.kind = _kinds[response.Kind] || CompletionItemKind.Property; + completion.insertText = response.CompletionText.replace(/<>/g, ''); + completion.commitCharacters = OmniSharpCompletionItemProvider.DefaultCommitCharacters; let array = completions[completion.label]; if (!array) { @@ -85,7 +97,7 @@ _kinds['Class'] = CompletionItemKind.Class; _kinds['Delegate'] = CompletionItemKind.Class; // need a better option for this. _kinds['Enum'] = CompletionItemKind.Enum; _kinds['Interface'] = CompletionItemKind.Interface; -_kinds['Struct'] = CompletionItemKind.Class; // need a better option for this. +_kinds['Struct'] = CompletionItemKind.Struct; // variables _kinds['Local'] = CompletionItemKind.Variable; @@ -93,11 +105,12 @@ _kinds['Parameter'] = CompletionItemKind.Variable; _kinds['RangeVariable'] = CompletionItemKind.Variable; // members -_kinds['EnumMember'] = CompletionItemKind.Property; // need a better option for this. -_kinds['Event'] = CompletionItemKind.Field; // need a better option for this. +_kinds['Const'] = CompletionItemKind.Constant; +_kinds['EnumMember'] = CompletionItemKind.EnumMember; +_kinds['Event'] = CompletionItemKind.Event; _kinds['Field'] = CompletionItemKind.Field; -_kinds['Property'] = CompletionItemKind.Property; _kinds['Method'] = CompletionItemKind.Method; +_kinds['Property'] = CompletionItemKind.Property; // other stuff _kinds['Label'] = CompletionItemKind.Unit; // need a better option for this. diff --git a/src/features/diagnosticsProvider.ts b/src/features/diagnosticsProvider.ts index ad108b00fa..1cc3ae9cd3 100644 --- a/src/features/diagnosticsProvider.ts +++ b/src/features/diagnosticsProvider.ts @@ -195,7 +195,7 @@ class DiagnosticsProvider extends AbstractSupport { let quickFixes = value.QuickFixes.filter(DiagnosticsProvider._shouldInclude); - // Easy case: If there are no diagnostics in the file, we can clear it quickly. + // Easy case: If there are no diagnostics in the file, we can clear it quickly. if (quickFixes.length === 0) { if (this._diagnostics.has(document.uri)) { this._diagnostics.delete(document.uri); @@ -273,7 +273,12 @@ class DiagnosticsProvider extends AbstractSupport { } private static _shouldInclude(quickFix: protocol.QuickFix): boolean { - return quickFix.LogLevel.toLowerCase() !== 'hidden'; + const config = vscode.workspace.getConfiguration('csharp'); + if (config.get('suppressHiddenDiagnostics', true)) { + return quickFix.LogLevel.toLowerCase() !== 'hidden'; + } else { + return true; + } } // --- data converter diff --git a/src/features/dotnetTest.ts b/src/features/dotnetTest.ts index aea373f4ea..5d2948ec3b 100644 --- a/src/features/dotnetTest.ts +++ b/src/features/dotnetTest.ts @@ -17,7 +17,7 @@ import * as path from 'path'; let _testOutputChannel: vscode.OutputChannel = undefined; function getTestOutputChannel(): vscode.OutputChannel { - if (_testOutputChannel == undefined) { + if (_testOutputChannel === undefined) { _testOutputChannel = vscode.window.createOutputChannel(".NET Test Log"); } @@ -36,58 +36,98 @@ export function registerDotNetTestDebugCommand(server: OmniSharpServer): vscode. (testMethod, fileName, testFrameworkName) => debugDotnetTest(testMethod, fileName, testFrameworkName, server)); } +function saveDirtyFiles(): Promise { + return Promise.resolve( + vscode.workspace.saveAll(/*includeUntitled*/ false)); +} + +function runTest(server: OmniSharpServer, fileName: string, testMethod: string, testFrameworkName: string): Promise { + const request: protocol.V2.RunTestRequest = { + FileName: fileName, + MethodName: testMethod, + TestFrameworkName: testFrameworkName + }; + + return serverUtils.runTest(server, request) + .then(response => response.Results); +} + +function reportResults(results: protocol.V2.DotNetTestResult[], output: vscode.OutputChannel): Promise { + const totalTests = results.length; + + let totalPassed = 0, totalFailed = 0, totalSkipped = 0; + for (let result of results) { + switch (result.Outcome) { + case protocol.V2.TestOutcomes.Failed: + totalFailed += 1; + break; + case protocol.V2.TestOutcomes.Passed: + totalPassed += 1; + break; + case protocol.V2.TestOutcomes.Skipped: + totalSkipped += 1; + break; + } + } + + output.appendLine(''); + output.appendLine(`Total tests: ${totalTests}. Passed: ${totalPassed}. Failed: ${totalFailed}. Skipped: ${totalSkipped}`); + output.appendLine(''); + + return Promise.resolve(); +} + // Run test through dotnet-test command. This function can be moved to a separate structure export function runDotnetTest(testMethod: string, fileName: string, testFrameworkName: string, server: OmniSharpServer) { const output = getTestOutputChannel(); output.show(); output.appendLine(`Running test ${testMethod}...`); + output.appendLine(''); - const disposable = server.onTestMessage(e => { + const listener = server.onTestMessage(e => { output.appendLine(e.Message); }); - const request: protocol.V2.RunTestRequest = { - FileName: fileName, - MethodName: testMethod, - TestFrameworkName: testFrameworkName - }; - - serverUtils.runTest(server, request) - .then(response => { - if (response.Pass) { - output.appendLine('Test passed \n'); - } - else { - output.appendLine('Test failed \n'); - } - - disposable.dispose(); - }, - reason => { + saveDirtyFiles() + .then(_ => runTest(server, fileName, testMethod, testFrameworkName)) + .then(results => reportResults(results, output)) + .then(() => listener.dispose()) + .catch(reason => { + listener.dispose(); vscode.window.showErrorMessage(`Failed to run test because ${reason}.`); - disposable.dispose(); }); } -function createLaunchConfiguration(program: string, argsString: string, cwd: string, debuggerEventsPipeName: string) { - let args = utils.splitCommandLineArgs(argsString); - - return { - // NOTE: uncomment this for vsdbg developement - // debugServer: 4711, - name: ".NET Test Launch", - type: "coreclr", - request: "launch", - debuggerEventsPipeName: debuggerEventsPipeName, - program, - args, - cwd, - stopAtEntry: true - }; +function createLaunchConfiguration(program: string, args: string, cwd: string, debuggerEventsPipeName: string) { + let debugOptions = vscode.workspace.getConfiguration('csharp').get('unitTestDebugingOptions'); + + // Get the initial set of options from the workspace setting + let result: any; + if (typeof debugOptions === "object") { + // clone the options object to avoid changing it + result = JSON.parse(JSON.stringify(debugOptions)); + } else { + result = {}; + } + + // Now fill in the rest of the options + result.name = ".NET Test Launch"; + result.type = "coreclr"; + result.request = "launch"; + result.debuggerEventsPipeName = debuggerEventsPipeName; + result.program = program; + result.args = args; + result.cwd = cwd; + + return result; } -function getLaunchConfigurationForVSTest(server: OmniSharpServer, fileName: string, testMethod: string, testFrameworkName: string, debugEventListener: DebugEventListener): Promise { +function getLaunchConfigurationForVSTest(server: OmniSharpServer, fileName: string, testMethod: string, testFrameworkName: string, debugEventListener: DebugEventListener, output: vscode.OutputChannel): Promise { + // Listen for test messages while getting start info. + const listener = server.onTestMessage(e => { + output.appendLine(e.Message); + }); const request: protocol.V2.DebugTestGetStartInfoRequest = { FileName: fileName, @@ -96,10 +136,18 @@ function getLaunchConfigurationForVSTest(server: OmniSharpServer, fileName: stri }; return serverUtils.debugTestGetStartInfo(server, request) - .then(response => createLaunchConfiguration(response.FileName, response.Arguments, response.WorkingDirectory, debugEventListener.pipePath())); + .then(response => { + listener.dispose(); + return createLaunchConfiguration(response.FileName, response.Arguments, response.WorkingDirectory, debugEventListener.pipePath()); + }); } -function getLaunchConfigurationForLegacy(server: OmniSharpServer, fileName: string, testMethod: string, testFrameworkName: string): Promise { +function getLaunchConfigurationForLegacy(server: OmniSharpServer, fileName: string, testMethod: string, testFrameworkName: string, output: vscode.OutputChannel): Promise { + // Listen for test messages while getting start info. + const listener = server.onTestMessage(e => { + output.appendLine(e.Message); + }); + const request: protocol.V2.GetTestStartInfoRequest = { FileName: fileName, MethodName: testMethod, @@ -107,16 +155,19 @@ function getLaunchConfigurationForLegacy(server: OmniSharpServer, fileName: stri }; return serverUtils.getTestStartInfo(server, request) - .then(response => createLaunchConfiguration(response.Executable, response.Argument, response.WorkingDirectory, null)); + .then(response => { + listener.dispose(); + return createLaunchConfiguration(response.Executable, response.Argument, response.WorkingDirectory, null); + }); } -function getLaunchConfiguration(server: OmniSharpServer, debugType: string, fileName: string, testMethod: string, testFrameworkName: string, debugEventListener: DebugEventListener): Promise { +function getLaunchConfiguration(server: OmniSharpServer, debugType: string, fileName: string, testMethod: string, testFrameworkName: string, debugEventListener: DebugEventListener, output: vscode.OutputChannel): Promise { switch (debugType) { - case "legacy": - return getLaunchConfigurationForLegacy(server, fileName, testMethod, testFrameworkName); - case "vstest": - return getLaunchConfigurationForVSTest(server, fileName, testMethod, testFrameworkName, debugEventListener); + case 'legacy': + return getLaunchConfigurationForLegacy(server, fileName, testMethod, testFrameworkName, output); + case 'vstest': + return getLaunchConfigurationForVSTest(server, fileName, testMethod, testFrameworkName, debugEventListener, output); default: throw new Error(`Unexpected debug type: ${debugType}`); @@ -129,25 +180,30 @@ export function debugDotnetTest(testMethod: string, fileName: string, testFramew // using VS Test. These require a different level of communication. let debugType: string; let debugEventListener: DebugEventListener = null; - let outputChannel = getTestOutputChannel(); - outputChannel.appendLine(`Debugging method '${testMethod}'.`); - return serverUtils.requestProjectInformation(server, { FileName: fileName }) + const output = getTestOutputChannel(); + + output.show(); + output.appendLine(`Debugging method '${testMethod}'...`); + output.appendLine(''); + + return saveDirtyFiles() + .then(_ => serverUtils.requestProjectInformation(server, { FileName: fileName })) .then(projectInfo => { if (projectInfo.DotNetProject) { - debugType = "legacy"; + debugType = 'legacy'; return Promise.resolve(); } else if (projectInfo.MsBuildProject) { - debugType = "vstest"; - debugEventListener = new DebugEventListener(fileName, server, outputChannel); + debugType = 'vstest'; + debugEventListener = new DebugEventListener(fileName, server, output); return debugEventListener.start(); } else { - throw new Error(); + throw new Error('Expected project.json or .csproj project.'); } }) - .then(() => getLaunchConfiguration(server, debugType, fileName, testMethod, testFrameworkName, debugEventListener)) + .then(() => getLaunchConfiguration(server, debugType, fileName, testMethod, testFrameworkName, debugEventListener, output)) .then(config => vscode.commands.executeCommand('vscode.startDebug', config)) .catch(reason => { vscode.window.showErrorMessage(`Failed to start debugger: ${reason}`); @@ -159,14 +215,21 @@ export function debugDotnetTest(testMethod: string, fileName: string, testFramew export function updateCodeLensForTest(bucket: vscode.CodeLens[], fileName: string, node: protocol.Node, isDebugEnable: boolean) { // backward compatible check: Features property doesn't present on older version OmniSharp - if (node.Features == undefined) { + if (node.Features === undefined) { return; } - let testFeature = node.Features.find(value => (value.Name == 'XunitTestMethod' || value.Name == 'NUnitTestMethod')); + let testFeature = node.Features.find(value => (value.Name == 'XunitTestMethod' || value.Name == 'NUnitTestMethod' || value.Name == 'MSTestMethod')); if (testFeature) { // this test method has a test feature - let testFrameworkName = testFeature.Name == 'XunitTestMethod' ? 'xunit' : 'nunit'; + let testFrameworkName = 'xunit'; + if (testFeature.Name == 'NunitTestMethod') { + testFrameworkName = 'nunit'; + } + else if (testFeature.Name == 'MSTestMethod') { + testFrameworkName = 'mstest'; + } + bucket.push(new vscode.CodeLens( toRange(node.Location), { title: "run test", command: 'dotnet.test.run', arguments: [testFeature.Data, fileName, testFrameworkName] })); @@ -208,6 +271,7 @@ class DebugEventListener { if (DebugEventListener.s_activeInstance !== null) { DebugEventListener.s_activeInstance.close(); } + DebugEventListener.s_activeInstance = this; this._serverSocket = net.createServer((socket: net.Socket) => { @@ -250,6 +314,7 @@ class DebugEventListener { this._outputChannel.appendLine("Warning: Communications error on debugger event channel. " + err.message); } }); + this._serverSocket.listen(this._pipePath, () => { isStarted = true; resolve(); diff --git a/src/features/processPicker.ts b/src/features/processPicker.ts index b7c4737ce0..77429ffc61 100644 --- a/src/features/processPicker.ts +++ b/src/features/processPicker.ts @@ -39,10 +39,19 @@ export class AttachPicker { } } +interface IPipeTransportOptions { + pipeProgram: string; + pipeArgs: string | string[]; + quoteArgs: boolean; +} + export class RemoteAttachPicker { public static get commColumnTitle() { return Array(PsOutputParser.secondColumnCharacters).join("a"); } public static get linuxPsCommand() { return `ps -axww -o pid=,comm=${RemoteAttachPicker.commColumnTitle},args=`; } public static get osxPsCommand() { return `ps -axww -o pid=,comm=${RemoteAttachPicker.commColumnTitle},args= -c`; } + public static get debuggerCommand() { return "${debuggerCommand}"; }; + public static get scriptShellCmd() { return "sh -s"; }; + private static _channel: vscode.OutputChannel = null; @@ -75,6 +84,112 @@ export class RemoteAttachPicker { }); } + // Note: osPlatform is passed as an argument for testing. + public static getPipeTransportOptions(pipeTransport: any, osPlatform: string): IPipeTransportOptions { + let pipeProgram: string = pipeTransport.pipeProgram; + let pipeArgs: string[] | string = pipeTransport.pipeArgs; + let quoteArgs: boolean = pipeTransport.quoteArgs != null ? pipeTransport.quoteArgs : true; // default value is true + let platformSpecificPipeTransportOptions: IPipeTransportOptions = this.getPlatformSpecificPipeTransportOptions(pipeTransport, osPlatform); + + if (platformSpecificPipeTransportOptions) { + pipeProgram = platformSpecificPipeTransportOptions.pipeProgram || pipeProgram; + pipeArgs = platformSpecificPipeTransportOptions.pipeArgs || pipeArgs; + quoteArgs = platformSpecificPipeTransportOptions.quoteArgs != null ? platformSpecificPipeTransportOptions.quoteArgs : quoteArgs; + } + + return { + pipeProgram: pipeProgram, + pipeArgs: pipeArgs, + quoteArgs: quoteArgs + }; + } + + // If the current process is on a current operating system and a specific pipe transport + // is included, then use that specific pipe transport configuration. + // + // Note: osPlatform is passed as an argument for testing. + private static getPlatformSpecificPipeTransportOptions(config: any, osPlatform: string): IPipeTransportOptions { + if (osPlatform === "darwin" && config.osx) { + return config.osx; + } else if (osPlatform === "linux" && config.linux) { + return config.linux; + } else if (osPlatform === "win32" && config.windows) { + return config.windows; + } + + return null; + } + + // Creates a pipe command string based on the type of pipe args. + private static createPipeCmd(pipeProgram: string, pipeArgs: string | string[], quoteArgs: boolean): Promise { + return this.ValidateAndFixPipeProgram(pipeProgram).then(fixedPipeProgram => { + if (typeof pipeArgs === "string") { + return Promise.resolve(this.createPipeCmdFromString(fixedPipeProgram, pipeArgs, quoteArgs)); + } + else if (pipeArgs instanceof Array) { + return Promise.resolve(this.createPipeCmdFromArray(fixedPipeProgram, pipeArgs, quoteArgs)); + } else { + // Invalid args type + return Promise.reject(new Error("pipeArgs must be a string or a string array type")); + } + }); + } + + public static createPipeCmdFromString(pipeProgram: string, pipeArgs: string, quoteArgs: boolean): string { + // Quote program if quoteArgs is true. + let pipeCmd: string = this.quoteArg(pipeProgram); + + // If ${debuggerCommand} exists in pipeArgs, replace. No quoting is applied to the command here. + if (pipeArgs.indexOf(this.debuggerCommand) >= 0) { + pipeCmd = pipeCmd.concat(" ", pipeArgs.replace(/\$\{debuggerCommand\}/g, this.scriptShellCmd)); + } + // Add ${debuggerCommand} to the end of the args. Quote if quoteArgs is true. + else { + pipeCmd = pipeCmd.concat(" ", pipeArgs.concat(" ", this.quoteArg(this.scriptShellCmd, quoteArgs))); + } + + return pipeCmd; + } + + public static createPipeCmdFromArray(pipeProgram: string, pipeArgs: string[], quoteArgs: boolean): string { + let pipeCmdList: string[] = []; + // Add pipeProgram to the start. Quoting is handeled later. + pipeCmdList.push(pipeProgram); + + // If ${debuggerCommand} exists, replace it. + if (pipeArgs.filter(arg => arg.indexOf(this.debuggerCommand) >= 0).length > 0) { + for (let arg of pipeArgs) { + while (arg.indexOf(this.debuggerCommand) >= 0) { + arg = arg.replace(this.debuggerCommand, RemoteAttachPicker.scriptShellCmd); + } + + pipeCmdList.push(arg); + } + } + // Add ${debuggerCommand} to the end of the arguments. + else { + pipeCmdList = pipeCmdList.concat(pipeArgs); + pipeCmdList.push(this.scriptShellCmd); + } + + // Quote if enabled. + return quoteArgs ? this.createArgumentList(pipeCmdList) : pipeCmdList.join(' '); + } + + // Quote the arg if the flag is enabled and there is a space. + public static quoteArg(arg: string, quoteArg: boolean = true): string { + if (quoteArg && arg.includes(' ')) { + return `"${arg}"`; + } + + return arg; + } + + // Converts an array of string arguments to a string version. Always quotes any arguments with spaces. + public static createArgumentList(args: string[]): string { + return args.map(arg => this.quoteArg(arg)).join(" "); + } + public static ShowAttachEntries(args: any): Promise { // Create remote attach output channel for errors. if (!RemoteAttachPicker._channel) { @@ -97,87 +212,22 @@ export class RemoteAttachPicker { return Promise.reject(new Error("Configuration \"" + name + "\" in launch.json does not have a " + "pipeTransport argument with debuggerPath for pickRemoteProcess. Use pickProcess for local attach.")); } else { - let pipeProgram: string = args.pipeTransport.pipeProgram; - let pipeArgs: string[] = args.pipeTransport.pipeArgs; - let quoteArgs: boolean = args.pipeTransport.quoteArgs != null ? args.pipeTransport.quoteArgs : true; // default value is true - let platformSpecificPipeTransportOptions: any = RemoteAttachPicker.getPlatformSpecificPipeTransportOptions(args); - - if (platformSpecificPipeTransportOptions) { - pipeProgram = platformSpecificPipeTransportOptions.pipeProgram || pipeProgram; - pipeArgs = platformSpecificPipeTransportOptions.pipeArgs || pipeArgs; - quoteArgs = platformSpecificPipeTransportOptions.pipeTransport.quoteArgs != null ? platformSpecificPipeTransportOptions.pipeTransport.quoteArgs : quoteArgs; - } + let pipeTransport = this.getPipeTransportOptions(args.pipeTransport, os.platform()); - return RemoteAttachPicker.ValidateAndFixPipeProgram(pipeProgram).then(pipeProgram => { - let pipeCmdList: string[] = []; - let scriptShellCmd: string = "sh -s"; - pipeCmdList.push(pipeProgram); - - const debuggerCommandString: string = "${debuggerCommand}"; - - if (pipeArgs.filter(arg => arg.indexOf(debuggerCommandString) >= 0).length > 0) { - for (let arg of pipeArgs) { - while (arg.indexOf("${debuggerCommand}") >= 0) { - arg = arg.replace("${debuggerCommand}", scriptShellCmd); - } - - pipeCmdList.push(arg); - } - } - else { - pipeCmdList = pipeCmdList.concat(pipeArgs); - pipeCmdList.push(scriptShellCmd); - } - - let pipeCmd: string = quoteArgs ? this.createArgumentList(pipeCmdList) : pipeCmdList.join(' '); - - return RemoteAttachPicker.getRemoteOSAndProcesses(pipeCmd).then(processes => { + return RemoteAttachPicker.createPipeCmd(pipeTransport.pipeProgram, pipeTransport.pipeArgs, pipeTransport.quoteArgs) + .then(pipeCmd => RemoteAttachPicker.getRemoteOSAndProcesses(pipeCmd)) + .then(processes => { let attachPickOptions: vscode.QuickPickOptions = { matchOnDescription: true, matchOnDetail: true, placeHolder: "Select the process to attach to" }; - return vscode.window.showQuickPick(processes, attachPickOptions).then(item => { - return item ? item.id : Promise.reject(new Error("Could not find a process id to attach.")); - }); - }); - }); + return vscode.window.showQuickPick(processes, attachPickOptions); + }) + .then(item => { return item ? item.id : Promise.reject(new Error("Could not find a process id to attach.")); }); } } - private static createArgumentList(args: string[]): string { - let ret = ""; - - for (let arg of args) { - if (ret) { - ret += " "; - } - - if (arg.includes(' ')) { - ret += `"${arg}"`; - } - else { - ret += `${arg}`; - } - } - - return ret; - } - - private static getPlatformSpecificPipeTransportOptions(config) { - const osPlatform = os.platform(); - - if (osPlatform == "darwin" && config.pipeTransport.osx) { - return config.pipeTransport.osx; - } else if (osPlatform == "linux" && config.pipeTransport.linux) { - return config.pipeTransport.linux; - } else if (osPlatform == "win32" && config.pipeTransport.windows) { - return config.pipeTransport.windows; - } - - return null; - } - public static getRemoteOSAndProcesses(pipeCmd: string): Promise { const scriptPath = path.join(getExtensionPath(), 'scripts', 'remoteProcessPickerScript'); diff --git a/src/omnisharp/protocol.ts b/src/omnisharp/protocol.ts index 2e03c51d34..86a7019461 100644 --- a/src/omnisharp/protocol.ts +++ b/src/omnisharp/protocol.ts @@ -538,7 +538,15 @@ export namespace V2 { TestFrameworkName: string; } - export interface DotNetResult { + export module TestOutcomes { + export const None = 'none'; + export const Passed = 'passed'; + export const Failed = 'failed'; + export const Skipped = 'skipped'; + export const NotFound = 'notfound'; + } + + export interface DotNetTestResult { MethodName: string; Outcome: string; ErrorMessage: string; @@ -548,6 +556,7 @@ export namespace V2 { export interface RunTestResponse { Failure: string; Pass: boolean; + Results: DotNetTestResult[]; } export interface TestMessageEvent { diff --git a/src/omnisharp/server.ts b/src/omnisharp/server.ts index a9c4fc0ab3..98c4447379 100644 --- a/src/omnisharp/server.ts +++ b/src/omnisharp/server.ts @@ -16,6 +16,7 @@ import TelemetryReporter from 'vscode-extension-telemetry'; import * as os from 'os'; import * as path from 'path'; import * as protocol from './protocol'; +import * as utils from '../common'; import * as vscode from 'vscode'; enum ServerState { @@ -337,9 +338,15 @@ export class OmniSharpServer { }); } else { - // Kill Unix process - this._serverProcess.kill('SIGTERM'); - cleanupPromise = Promise.resolve(); + // Kill Unix process and children + cleanupPromise = utils.getUnixChildProcessIds(this._serverProcess.pid) + .then(children => { + for (let child of children) { + process.kill(child, 'SIGTERM'); + } + + this._serverProcess.kill('SIGTERM'); + }); } return cleanupPromise.then(() => { diff --git a/src/tools/OptionsSchema.json b/src/tools/OptionsSchema.json index ea916a757a..012d5fade9 100644 --- a/src/tools/OptionsSchema.json +++ b/src/tools/OptionsSchema.json @@ -25,12 +25,21 @@ "default": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'" }, "pipeArgs": { - "type": "array", - "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", - "items": { - "type": "string" - }, - "default": [] + "anyOf": [ + { + "type": "array", + "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "items": { + "type": "string" + }, + "default": [] + }, + { + "type": "string", + "description": "Stringified version of command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "default": "" + } + ] }, "quoteArgs": { "type": "boolean", @@ -69,12 +78,21 @@ "default": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'" }, "pipeArgs": { - "type": "array", - "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", - "items": { - "type": "string" - }, - "default": [] + "anyOf": [ + { + "type": "array", + "description": "Command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "items": { + "type": "string" + }, + "default": [] + }, + { + "type": "string", + "description": "Stringified version of command line arguments passed to the pipe program. Token ${debuggerCommand} in pipeArgs will get replaced by the full debugger command, this token can be specified inline with other arguments. If ${debuggerCommand} isn’t used in any argument, the full debugger command will be instead be added to the end of the argument list.", + "default": "" + } + ] }, "debuggerPath" : { "type" : "string", @@ -195,9 +213,21 @@ "default": true }, "args": { - "type": "string", - "description": "The arguments to pass to the command to open the browser. Use ${auto-detect-url} to automatically use the address the server is listening to", - "default": "${auto-detect-url}" + "anyOf": [ + { + "type": "array", + "description": "Command line arguments passed to the program.", + "items": { + "type": "string" + }, + "default": [] + }, + { + "type": "string", + "description": "Stringified version of command line arguments passed to the program.", + "default": "" + } + ] }, "osx": { "$ref": "#/definitions/LaunchBrowserPlatformOptions", @@ -226,8 +256,7 @@ "LaunchOptions": { "type": "object", "required": [ - "program", - "cwd" + "program" ], "properties": { "program": { @@ -241,12 +270,21 @@ "default": "${workspaceRoot}" }, "args": { - "type": "array", - "description": "Command line arguments passed to the program.", - "items": { - "type": "string" - }, - "default": [] + "anyOf": [ + { + "type": "array", + "description": "Command line arguments passed to the program.", + "items": { + "type": "string" + }, + "default": [] + }, + { + "type": "string", + "description": "Stringified version of command line arguments passed to the program.", + "default": "" + } + ] }, "stopAtEntry": { "type": "boolean", diff --git a/src/tools/README.md b/src/tools/README.md index bab72413f2..0e1a6b06fa 100644 --- a/src/tools/README.md +++ b/src/tools/README.md @@ -5,8 +5,12 @@ OptionsSchema.json defines the type for Launch/Attach options. If there are any modifications to the OptionsSchema.json file. Please run `gulp generateOptionsSchema` at the repo root. This will call GenerateOptionsSchema and update the package.json file. -**NOTE:** *Any manual changes to package.json's object.contributes.debuggers[0].configurationAttributes will be -replaced by this generator* +### Important notes: + +1. Any manual changes to package.json's object.contributes.debuggers[0].configurationAttributes will be +replaced by this generator. +2. This does **NOT** update the schema for csharp.unitTestDebugingOptions. So if the schema change is something valuable in unit test debugging, consider updating that section of package.json (look for `"csharp.unitTestDebugingOptions"`). The schema will work even if this step is omitted, but users will not get IntelliSense help when editing the new option if this step is skipped. + If there is any other type of options added in the future, you will need to modify the GenerateOptionsSchema function to have it appear in package.json. It only adds launch and attach. diff --git a/test/common.test.ts b/test/common.test.ts index 0624f7581e..cf90b15fe0 100644 --- a/test/common.test.ts +++ b/test/common.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { should } from 'chai'; -import { buildPromiseChain, splitCommandLineArgs, safeLength, sum } from '../src/common'; +import { buildPromiseChain, safeLength, sum } from '../src/common'; suite("Common", () => { suiteSetup(() => should()); @@ -25,50 +25,6 @@ suite("Common", () => { }); }); - suite("splitCommandLineArgs", () => { - test("single argument", () => { - let result = splitCommandLineArgs("hello"); - - result.should.deep.equal(["hello"]); - }); - - test("two arguments", () => { - let result = splitCommandLineArgs("hello world"); - - result.should.deep.equal(["hello", "world"]); - }); - - test("single quoted argument", () => { - let result = splitCommandLineArgs("\"hello world\""); - - result.should.deep.equal(["hello world"]); - }); - - test("two argument, one quoted", () => { - let result = splitCommandLineArgs("hello \"world\""); - - result.should.deep.equal(["hello", "world"]); - }); - - test("many quoted arguments", () => { - let result = splitCommandLineArgs("\"a\" \"b\" \"c\" \"d\" \"e\""); - - result.should.deep.equal(["a", "b", "c", "d", "e"]); - }); - - test("many arguments, some quoted, some not", () => { - let result = splitCommandLineArgs("\"a\" b \"c\" d \"e\""); - - result.should.deep.equal(["a", "b", "c", "d", "e"]); - }); - - test("many arguments, some quoted, some not (inverted)", () => { - let result = splitCommandLineArgs("a \"b\" c \"d\" e"); - - result.should.deep.equal(["a", "b", "c", "d", "e"]); - }); - }); - suite("safeLength", () => { test("return 0 for empty array", () => { let array = []; diff --git a/test/processPicker.test.ts b/test/processPicker.test.ts new file mode 100644 index 0000000000..63ad82f8f5 --- /dev/null +++ b/test/processPicker.test.ts @@ -0,0 +1,233 @@ +import { should } from 'chai'; +import { RemoteAttachPicker } from '../src/features/processPicker'; + +suite("Remote Process Picker: Validate quoting arguments.", () => { + suiteSetup(() => should()); + test("Argument with no spaces", () => { + let nonQuotedArg = RemoteAttachPicker.quoteArg("C:\\Users\\nospace\\program.exe"); + + nonQuotedArg.should.deep.equal("C:\\Users\\nospace\\program.exe"); + }); + + test("Argument with spaces", () => { + let nonQuotedArg = RemoteAttachPicker.quoteArg("C:\\Users\\s p a c e\\program.exe"); + + nonQuotedArg.should.deep.equal("\"C:\\Users\\s p a c e\\program.exe\""); + }); + + test("Argument with spaces with no quotes", () => { + let nonQuotedArg = RemoteAttachPicker.quoteArg("C:\\Users\\s p a c e\\program.exe", false); + + nonQuotedArg.should.deep.equal("C:\\Users\\s p a c e\\program.exe"); + }); + + test("WSL with array arguments and quote args", () => { + let pipeTransport = GetWindowsWSLLaunchJSONWithArrayArgs(); + + let pipeCmd = RemoteAttachPicker.createPipeCmdFromArray(pipeTransport.pipeProgram, pipeTransport.pipeArgs, true); + + pipeCmd.should.deep.equal("C:\\System32\\bash.exe -c \"" + RemoteAttachPicker.scriptShellCmd + "\""); + }); + + test("WSL with array arguments and no quote args", () => { + let pipeTransport = GetWindowsWSLLaunchJSONWithArrayArgs(); + + let pipeCmd = RemoteAttachPicker.createPipeCmdFromArray(pipeTransport.pipeProgram, pipeTransport.pipeArgs, false); + + pipeCmd.should.deep.equal("C:\\System32\\bash.exe -c " + RemoteAttachPicker.scriptShellCmd); + }); + + test("WSL with array arguments + debugger command and quote args", () => { + let pipeTransport = GetWindowsWSLLaunchJSONWithArrayArgsAndDebuggerCommand(); + + let pipeCmd = RemoteAttachPicker.createPipeCmdFromArray(pipeTransport.pipeProgram, pipeTransport.pipeArgs, true); + + pipeCmd.should.deep.equal("C:\\System32\\bash.exe -c \"" + RemoteAttachPicker.scriptShellCmd + "\" -- ignored"); + }); + + test("WSL with array arguments + debugger command and no quote args", () => { + let pipeTransport = GetWindowsWSLLaunchJSONWithArrayArgsAndDebuggerCommand(); + + let pipeCmd = RemoteAttachPicker.createPipeCmdFromArray(pipeTransport.pipeProgram, pipeTransport.pipeArgs, false); + + pipeCmd.should.deep.equal("C:\\System32\\bash.exe -c " + RemoteAttachPicker.scriptShellCmd + " -- ignored"); + }); + + test("WSL with string arguments and quote args", () => { + let pipeTransport = GetWindowsWSLLaunchJSONWithStringArgs(); + + let pipeCmd = RemoteAttachPicker.createPipeCmdFromString(pipeTransport.pipeProgram, pipeTransport.pipeArgs, true); + + pipeCmd.should.deep.equal("C:\\System32\\bash.exe -c \"" + RemoteAttachPicker.scriptShellCmd + "\""); + }); + + test("WSL with string arguments and no quote args", () => { + let pipeTransport = GetWindowsWSLLaunchJSONWithStringArgs(); + + let pipeCmd = RemoteAttachPicker.createPipeCmdFromString(pipeTransport.pipeProgram, pipeTransport.pipeArgs, false); + + pipeCmd.should.deep.equal("C:\\System32\\bash.exe -c " + RemoteAttachPicker.scriptShellCmd); + }); + + test("WSL with string arguments + debugger command and quote args", () => { + let pipeTransport = GetWindowsWSLLaunchJSONWithStringArgsAndDebuggerCommand(); + + let pipeCmd = RemoteAttachPicker.createPipeCmdFromString(pipeTransport.pipeProgram, pipeTransport.pipeArgs, true); + + pipeCmd.should.deep.equal("C:\\System32\\bash.exe -c " + RemoteAttachPicker.scriptShellCmd + " -- ignored"); + }); + + test("WSL with string arguments + debugger command and no quote args", () => { + let pipeTransport = GetWindowsWSLLaunchJSONWithStringArgsAndDebuggerCommand(); + + let pipeCmd = RemoteAttachPicker.createPipeCmdFromString(pipeTransport.pipeProgram, pipeTransport.pipeArgs, false); + + pipeCmd.should.deep.equal("C:\\System32\\bash.exe -c " + RemoteAttachPicker.scriptShellCmd + " -- ignored"); + }); + + test("Windows Docker with string args, debuggerCommand", () => { + let pipeTransport = GetWindowsDockerLaunchJSONWithStringArgsAndDebuggerCommand(); + + // quoteArgs flag should be ignored + let pipeCmd = RemoteAttachPicker.createPipeCmdFromString(pipeTransport.pipeProgram, pipeTransport.pipeArgs, pipeTransport.quoteArgs); + + pipeCmd.should.deep.equal("docker -i exec 1234567 " + RemoteAttachPicker.scriptShellCmd); + }); + + test("Windows Docker with array args", () => { + let pipeTransport = GetWindowsDockerLaunchJSONWithArrayArgs(); + + let pipeCmd = RemoteAttachPicker.createPipeCmdFromArray(pipeTransport.pipeProgram, pipeTransport.pipeArgs, pipeTransport.quoteArgs); + + pipeCmd.should.deep.equal("docker -i exec 1234567 " + RemoteAttachPicker.scriptShellCmd); + + }); + + test("Windows Docker with array args with quotes", () => { + let pipeTransport = GetWindowsDockerLaunchJSONWithArrayArgs(); + + let pipeCmd = RemoteAttachPicker.createPipeCmdFromArray(pipeTransport.pipeProgram, pipeTransport.pipeArgs, true); + + pipeCmd.should.deep.equal("docker -i exec 1234567 \"" + RemoteAttachPicker.scriptShellCmd + "\""); + + }); + + test("Linux dotnet with array args and spaces", () => { + let pipeTransport = GetLinuxLaunchJSONWithArrayArgs(); + + let pipeCmd = RemoteAttachPicker.createPipeCmdFromArray(pipeTransport.pipeProgram, pipeTransport.pipeArgs, true); + + pipeCmd.should.deep.equal(`/usr/bin/shared/dotnet bin/framework/myprogram.dll \"argument with spaces\" \"${RemoteAttachPicker.scriptShellCmd}\"`); + }); + + test("Multiple ${debuggerCommand} in string args", () => { + let pipeCmd = RemoteAttachPicker.createPipeCmdFromString("program.exe", "".concat(RemoteAttachPicker.debuggerCommand, " ", RemoteAttachPicker.debuggerCommand, " ", RemoteAttachPicker.debuggerCommand), true); + + pipeCmd.should.deep.equal("program.exe " + RemoteAttachPicker.scriptShellCmd + " " + RemoteAttachPicker.scriptShellCmd + " " + RemoteAttachPicker.scriptShellCmd); + }); + + test("Multiple ${debuggerCommand} in array args", () => { + let pipeCmd = RemoteAttachPicker.createPipeCmdFromArray("program.exe", [RemoteAttachPicker.debuggerCommand, RemoteAttachPicker.debuggerCommand, RemoteAttachPicker.debuggerCommand], true); + + pipeCmd.should.deep.equal("program.exe \"" + RemoteAttachPicker.scriptShellCmd + "\" \"" + RemoteAttachPicker.scriptShellCmd + "\" \"" + RemoteAttachPicker.scriptShellCmd + "\""); + }); + + test("OS Specific Configurations", () => { + let launch = GetOSSpecificJSON(); + + let pipeTransport = RemoteAttachPicker.getPipeTransportOptions(launch, "win32"); + + pipeTransport.pipeProgram.should.deep.equal("Windows pipeProgram"); + pipeTransport.pipeArgs.should.deep.equal("windows"); + + pipeTransport = RemoteAttachPicker.getPipeTransportOptions(launch, "darwin"); + + pipeTransport.pipeProgram.should.deep.equal("OSX pipeProgram"); + pipeTransport.pipeArgs.should.deep.equal(["osx"]); + + pipeTransport = RemoteAttachPicker.getPipeTransportOptions(launch, "linux"); + + pipeTransport.pipeProgram.should.deep.equal("Linux pipeProgram"); + // Linux pipeTransport does not have args defined, should use the one defined in pipeTransport. + pipeTransport.pipeArgs.should.deep.equal([]); + + }); +}); + +function GetWindowsWSLLaunchJSONWithArrayArgs() { + return { + pipeCwd: "${workspaceRoot}", + pipeProgram: "C:\\System32\\bash.exe", + pipeArgs: ["-c"] + } +} + +function GetWindowsWSLLaunchJSONWithArrayArgsAndDebuggerCommand() { + return { + pipeCwd: "${workspaceRoot}", + pipeProgram: "C:\\System32\\bash.exe", + pipeArgs: ["-c", "${debuggerCommand}", "--", "ignored"] + } +} + +function GetWindowsWSLLaunchJSONWithStringArgs() { + return { + pipeCwd: "${workspaceRoot}", + pipeProgram: "C:\\System32\\bash.exe", + pipeArgs: "-c" + } +} + +function GetWindowsWSLLaunchJSONWithStringArgsAndDebuggerCommand() { + return { + pipeCwd: "${workspaceRoot}", + pipeProgram: "C:\\System32\\bash.exe", + pipeArgs: "-c ${debuggerCommand} -- ignored" + } +} + +function GetWindowsDockerLaunchJSONWithArrayArgs() { + return { + pipeCwd: "${workspaceRoot}", + pipeProgram: "docker", + pipeArgs: ["-i", "exec", "1234567"], + quoteArgs: false + } +}; + +function GetWindowsDockerLaunchJSONWithStringArgsAndDebuggerCommand() { + return { + pipeCwd: "${workspaceRoot}", + pipeProgram: "docker", + pipeArgs: "-i exec 1234567 ${debuggerCommand}", + quoteArgs: false + } +} + +function GetLinuxLaunchJSONWithArrayArgs() { + return { + pipeCwd: "${workspaceRoot}", + pipeProgram: "/usr/bin/shared/dotnet", + pipeArgs: ["bin/framework/myprogram.dll", "argument with spaces"], + quoteArg: true + } +} + +function GetOSSpecificJSON() { + return { + pipeCwd: "${workspaceRoot}", + pipeProgram: "pipeProgram", + pipeArgs: [], + windows: { + pipeProgram: "Windows pipeProgram", + pipeArgs: "windows" + }, + osx: { + pipeProgram: "OSX pipeProgram", + pipeArgs: ["osx"] + }, + linux: { + pipeProgram: "Linux pipeProgram", + } + } +} \ No newline at end of file