diff --git a/.travis.yml b/.travis.yml index 36a2c807a5..ff571682bd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,7 @@ addons: - ubuntu-toolchain-r-test packages: - g++-4.9 + - libsecret-1-dev install: - npm install diff --git a/CHANGELOG.md b/CHANGELOG.md index 405168c53e..6568ee24a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,50 @@ -## Known Issues in 1.12.0 +## Known Issues in 1.13.0 * There currently is no completion support for package references in csproj files. ([#1156](https://github.com/OmniSharp/omnisharp-vscode/issues/1156)) + * As an alternative, consider installing the [MSBuild Project Tools](https://marketplace.visualstudio.com/items?itemName=tintoy.msbuild-project-tools) extension by @tintoy. + +## 1.13.0 _(Not Yet Released)_ + +#### Cake + +* Added support for *.cake files! (PRs: [#1681](https://github.com/OmniSharp/omnisharp-vscode/pull/1681), [omnisharp-roslyn#932](https://github.com/OmniSharp/omnisharp-roslyn/pull/932)) _(Contributed by [@mholo65](https://github.com/mholo65))_ + +#### Debugger + +* Improved logic for resolving breakpoints in local functions and lambdas. ([#1678](https://github.com/OmniSharp/omnisharp-vscode/issues/1678)) +* When generating a new launch.json file via "start debugging" in a workspace without a launch.json file, the extension now provides the same content as is created with the '.NET: Generate Assets for Build and Debug' command. This takes advantage of a new extensibility point from VS Code. Before the C# extension could only statically provide templates, so, for example, they couldn't have the path to the launchable project. (PR: [#1801](https://github.com/OmniSharp/omnisharp-vscode/pull/1801)) + +#### Editor + +* Improved completion list behavior when matching substrings. (PRs: [#1813](https://github.com/OmniSharp/omnisharp-vscode/pull/1813), [omnisharp-roslyn#990](https://github.com/OmniSharp/omnisharp-roslyn/pull/990)) _(Contributed by [@filipw](https://github.com/filipw))_ +* Completion list now triggers on SPACE after `new`. ([#146](https://github.com/OmniSharp/omnisharp-vscode/issues/146), PR: [#1776](https://github.com/OmniSharp/omnisharp-vscode/pull/1776), [omnisharp-roslyn#975](https://github.com/OmniSharp/omnisharp-roslyn/pull/975)) + +#### Navigation + +* Fixed issue with Go to Definition where it was not possible to navigate to a definition within the same file if the file was generated from metadata. (PR: [#1772](https://github.com/OmniSharp/omnisharp-vscode/pull/1772)) _(Contributed by [@filipw](https://github.com/filipw))_ +* Improved symbol search behavior when matching substrings. (PR: [omnisharp-roslyn#990](https://github.com/OmniSharp/omnisharp-roslyn/pull/990)) _(Contributed by [@filipw](https://github.com/filipw))_ + +#### Project System + +* Significantly changed how MSBuild is located by OmniSharp, resulting in more project types loading properly. (PR: [omnisharp-roslyn#988](https://github.com/OmniSharp/omnisharp-roslyn/pull/988)) +* Fixed bug where `LangVersion` property was not read correctly from project file, blocking C# 7.1 development. ([omnisharp-roslyn#961](https://github.com/OmniSharp/omnisharp-roslyn/issues/961), PR: [omnisharp-roslyn#962](https://github.com/OmniSharp/omnisharp-roslyn/pull/962)]) _(Contributed by [@filipw](https://github.com/filipw))_ +* Fixed issue where signing key was not read correctly from project file, which can result in InternalsVisibleTo not being handled properly. (PR: [omnisharp-roslyn#964](https://github.com/OmniSharp/omnisharp-roslyn/pull/964)) +* Fixed long-standing problem with renaming files. ([#785](https://github.com/OmniSharp/omnisharp-vscode/issues/785), [#1792](https://github.com/OmniSharp/omnisharp-vscode/issues/1792), PR: [#1805](https://github.com/OmniSharp/omnisharp-vscode/pull/1805)) +* Fixed problem where the Antlr4.CodeGenerator Nuget package would not generate files during OmniSharp design-time build. ([#1822](https://github.com/OmniSharp/omnisharp-vscode/issues/1822), PR: [omnisharp-roslyn#1002](https://github.com/OmniSharp/omnisharp-roslyn/pull/1002)) +* Fixed issue where a C# project referencing a non-C# project would cause the referenced project to be loaded (causing OmniSharp to potentially treat it as C#!). ([#371](https://github.com/OmniSharp/omnisharp-vscode/issues/371), [#1829](https://github.com/OmniSharp/omnisharp-vscode/issues/1829), PR: [omnisharp-roslyn#1005](https://github.com/OmniSharp/omnisharp-roslyn/pull/1005)) + +#### Testing + +* Fix error that occurs when running or debugging tests with latest xUnit 2.3.0 builds. ([#1733](https://github.com/OmniSharp/omnisharp-vscode/issues/1733), [omnisharp-rolsyn#944](https://github.com/OmniSharp/omnisharp-roslyn/issues/944), PR: [omnisharp-roslyn#945](https://github.com/OmniSharp/omnisharp-roslyn/pull/945), [#1749](https://github.com/OmniSharp/omnisharp-vscode/pull/1749)) +* Fix issue causing NUnit tests not to work when running or debugging tests. ([#1615](https://github.com/OmniSharp/omnisharp-vscode/issues/1635), PR: [#1760](https://github.com/OmniSharp/omnisharp-vscode/pull/1760)). _(Contributed by [@dgileadi](https://github.com/dgileadi))_ +* Pass `--no-restore` when invoking `dotnet build` to ensure that implicit restore does not run, making the build and the test run a bit faster. ([omnisharp-roslyn##942](https://github.com/OmniSharp/omnisharp-roslyn/issues/942), PR: [omnisharp-roslyn#945](https://github.com/OmniSharp/omnisharp-roslyn/pull/945)) +* Fix Unit Test debugging with VS Code 1.18. ([#1800](https://github.com/OmniSharp/omnisharp-vscode/issues/1800)) + +#### Other Updates and Fixes + +* If Mono 5.2.0 or greater is installed, OmniSharp will be launched on that rather than the local Mono runtime that it carries with it. This allows, more Mono and Xamarin projects to load properly and improves OmniSharp performance (since it can run on Mono AOT'd binaries). ([#1779](https://github.com/OmniSharp/omnisharp-vscode/issues/1779), PR: [#1789](https://github.com/OmniSharp/omnisharp-vscode/pull/1789)) +* Support added for launching OmniSharp on folders and solutions across multi-root workspaces. ([#1762](https://github.com/OmniSharp/omnisharp-vscode/issues/1762), PR: [#1806](https://github.com/OmniSharp/omnisharp-vscode/pull/1806)) +* Added `csharp.referencesCodeLens.enabled` and `csharp.testsCodeLens.enabled` options to allow disabling/enabling for the 'references' and 'run/debug test' code lenses independently. ([#1570](https://github.com/OmniSharp/omnisharp-vscode/issues/1570), [#1807](https://github.com/OmniSharp/omnisharp-vscode/issues/1807), PRs: [#1781](https://github.com/OmniSharp/omnisharp-vscode/pull/1781), [#1809](https://github.com/OmniSharp/omnisharp-vscode/pull/1809)) ## 1.12.1 (August 14, 2017) @@ -426,4 +470,4 @@ There have been several fixes to the colorizer grammar resulting in much smoothe * Adds debugger support for new Linux versions: Ubuntu 16.04, Fedora 23, openSUSE 13.2, and Oracle Linux 7.1 * Enhanced debug console output: module loads are now output, and there are launch.json options for controlling what is output * Source file checksum support for breakpoints. This ensures that the debugger only sets breakpoints in code that exactly matches the open document. -* Support for editing the value of variables in the watch and locals window (requires VS Code 1.3) \ No newline at end of file +* Support for editing the value of variables in the watch and locals window (requires VS Code 1.3) diff --git a/debugger-launchjson.md b/debugger-launchjson.md index 0c0df85c0a..644aee9be8 100644 --- a/debugger-launchjson.md +++ b/debugger-launchjson.md @@ -20,9 +20,9 @@ This will create a task that runs `dotnet build`. You can read more about tasks ## Program The program field is set to the path of the application dll or .NET Core host executable to launch. -This property normally takes the form: "${workspaceRoot}/bin/Debug/\/\". +This property normally takes the form: "${workspaceFolder}/bin/Debug/\/\". -Example: `"${workspaceRoot}/bin/Debug/netcoreapp1.1/MyProject.dll"` +Example: `"${workspaceFolder}/bin/Debug/netcoreapp1.1/MyProject.dll"` Where: @@ -63,7 +63,7 @@ When this is set to `externalTerminal` the target process will run in a separate You can optionally configure a file by file mapping by providing map following this schema: "sourceFileMap": { - "C:\foo":"/home/me/foo" + "C:\\foo":"/home/me/foo" } ## Symbol Path @@ -102,7 +102,7 @@ then add the pipeTransport field folloing this schema: "pipeProgram": "ssh", "pipeArgs": [ "-T", "ExampleAccount@ExampleTargetComputer" ], "debuggerPath": "~/vsdbg/vsdbg", - "pipeCwd": "${workspaceRoot}", + "pipeCwd": "${workspaceFolder}", "quoteArgs": true } diff --git a/debugger.md b/debugger.md index d8787a64fd..82a394fb24 100644 --- a/debugger.md +++ b/debugger.md @@ -66,7 +66,7 @@ If your code has multiple projects or you would rather generate these files by h "configurations": [ { ... - "program": "${workspaceRoot}/MyLaunchingProject/bin/Debug/netcoreapp1.0/MyLaunchingProject.dll", + "program": "${workspaceFolder}/MyLaunchingProject/bin/Debug/netcoreapp1.0/MyLaunchingProject.dll", ##### 4: Start debugging Your project is now all set. Set a breakpoint or two where you want to stop, click the debugger play button (or press F5) and you are off. diff --git a/package.json b/package.json index 903e310085..959705d77a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "csharp", "publisher": "ms-vscode", - "version": "1.12.1", + "version": "1.13.0-beta5", "description": "C# for Visual Studio Code (powered by OmniSharp).", "displayName": "C#", "author": "Microsoft Corporation", @@ -30,7 +30,7 @@ "postinstall": "node ./node_modules/vscode/bin/install" }, "dependencies": { - "fs-extra": "^1.0.0", + "fs-extra": "^4.0.2", "http-proxy-agent": "^1.0.0", "https-proxy-agent": "^1.0.0", "jsonc-parser": "^0.3.0", @@ -46,7 +46,7 @@ }, "devDependencies": { "@types/chai": "^3.4.34", - "@types/fs-extra": "0.0.35", + "@types/fs-extra": "4.0.3", "@types/mkdirp": "^0.3.29", "@types/mocha": "^2.2.32", "@types/node": "^6.0.40", @@ -63,13 +63,13 @@ "tslint-microsoft-contrib": "^2.0.12", "typescript": "^2.0.3", "vsce": "^1.7.0", - "vscode": "^1.0.3" + "vscode": "^1.1.6" }, "runtimeDependencies": [ { "description": "OmniSharp for Windows (.NET 4.6 / x86)", - "url": "https://download.visualstudio.microsoft.com/download/pr/11114591/53def2e097a7e2c1a8bf567c6d81ad7a/omnisharp-win-x86-1.23.2.zip", - "fallbackUrl": "https://omnisharpdownload.blob.core.windows.net/ext/omnisharp-win-x86-1.23.2.zip", + "url": "https://download.visualstudio.microsoft.com/download/pr/100265554/077ad93cf1fb671e2c0902586873ab06/omnisharp-win-x86-1.26.1.zip", + "fallbackUrl": "https://omnisharpdownload.blob.core.windows.net/ext/omnisharp-win-x86-1.26.1.zip", "installPath": ".omnisharp", "platforms": [ "win32" @@ -81,8 +81,8 @@ }, { "description": "OmniSharp for Windows (.NET 4.6 / x64)", - "url": "https://download.visualstudio.microsoft.com/download/pr/11114590/f5469e805c6dcbe675e6a1d94fd78cc0/omnisharp-win-x64-1.23.2.zip", - "fallbackUrl": "https://omnisharpdownload.blob.core.windows.net/ext/omnisharp-win-x64-1.23.2.zip", + "url": "https://download.visualstudio.microsoft.com/download/pr/100265553/6af3cbbb63bb25dc4f2b481879e8525a/omnisharp-win-x64-1.26.1.zip", + "fallbackUrl": "https://omnisharpdownload.blob.core.windows.net/ext/omnisharp-win-x64-1.26.1.zip", "installPath": ".omnisharp", "platforms": [ "win32" @@ -94,8 +94,8 @@ }, { "description": "OmniSharp for OSX", - "url": "https://download.visualstudio.microsoft.com/download/pr/11114589/63b1de65cb20eb8f423449f864e1ac97/omnisharp-osx-1.23.2.zip", - "fallbackUrl": "https://omnisharpdownload.blob.core.windows.net/ext/omnisharp-osx-1.23.2.zip", + "url": "https://download.visualstudio.microsoft.com/download/pr/100265552/a9ff84a7405589572ade0087cdf2c2b5/omnisharp-osx-1.26.1.zip", + "fallbackUrl": "https://omnisharpdownload.blob.core.windows.net/ext/omnisharp-osx-1.26.1.zip", "installPath": ".omnisharp", "platforms": [ "darwin" @@ -108,8 +108,8 @@ }, { "description": "OmniSharp for Linux (x86)", - "url": "https://download.visualstudio.microsoft.com/download/pr/11114588/1b7020f25d4bb682983b831d97810489/omnisharp-linux-x86-1.23.2.zip", - "fallbackUrl": "https://omnisharpdownload.blob.core.windows.net/ext/omnisharp-linux-x86-1.23.2.zip", + "url": "https://download.visualstudio.microsoft.com/download/pr/100265551/666592bdb3277d5152c0930f092b7c1c/omnisharp-linux-x86-1.26.1.zip", + "fallbackUrl": "https://omnisharpdownload.blob.core.windows.net/ext/omnisharp-linux-x86-1.26.1.zip", "installPath": ".omnisharp", "platforms": [ "linux" @@ -126,8 +126,8 @@ }, { "description": "OmniSharp for Linux (x64)", - "url": "https://download.visualstudio.microsoft.com/download/pr/11114587/5d10adc99d8c7e561dadc2ab31031397/omnisharp-linux-x64-1.23.2.zip", - "fallbackUrl": "https://omnisharpdownload.blob.core.windows.net/ext/omnisharp-linux-x64-1.23.2.zip", + "url": "https://download.visualstudio.microsoft.com/download/pr/100265550/6780b525c18ff3bdb4687fa8e7657d6c/omnisharp-linux-x64-1.26.1.zip", + "fallbackUrl": "https://omnisharpdownload.blob.core.windows.net/ext/omnisharp-linux-x64-1.26.1.zip", "installPath": ".omnisharp", "platforms": [ "linux" @@ -143,8 +143,8 @@ }, { "description": ".NET Core Debugger (Windows / x64)", - "url": "https://download.visualstudio.microsoft.com/download/pr/10975649/e7e53245607ac00f315a629e2ed934b9/coreclr-debug-win7-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-12-5/coreclr-debug-win7-x64.zip", + "url": "https://download.visualstudio.microsoft.com/download/pr/11121545/61b8ee7aa56d99502a7ae3a19547e73b/coreclr-debug-win7-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-13-0/coreclr-debug-win7-x64.zip", "installPath": ".debugger", "platforms": [ "win32" @@ -156,8 +156,8 @@ }, { "description": ".NET Core Debugger (macOS / x64)", - "url": "https://download.visualstudio.microsoft.com/download/pr/10975649/e7e53245607ac00f315a629e2ed934b9/coreclr-debug-osx.10.11-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-12-5/coreclr-debug-osx.10.11-x64.zip", + "url": "https://download.visualstudio.microsoft.com/download/pr/11121545/61b8ee7aa56d99502a7ae3a19547e73b/coreclr-debug-osx.10.11-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-13-0/coreclr-debug-osx.10.11-x64.zip", "installPath": ".debugger", "platforms": [ "darwin" @@ -173,8 +173,8 @@ }, { "description": ".NET Core Debugger (linux / x64)", - "url": "https://download.visualstudio.microsoft.com/download/pr/10975649/e7e53245607ac00f315a629e2ed934b9/coreclr-debug-linux-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-12-5/coreclr-debug-linux-x64.zip", + "url": "https://download.visualstudio.microsoft.com/download/pr/11121545/61b8ee7aa56d99502a7ae3a19547e73b/coreclr-debug-linux-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-13-0/coreclr-debug-linux-x64.zip", "installPath": ".debugger", "platforms": [ "linux" @@ -190,9 +190,10 @@ } ], "engines": { - "vscode": "^1.12.0" + "vscode": "^1.17.0" }, "activationEvents": [ + "onDebug", "onLanguage:csharp", "onCommand:o.restart", "onCommand:o.pickProjectAndStart", @@ -206,14 +207,21 @@ "workspaceContains:*.csproj", "workspaceContains:*.sln", "workspaceContains:*.csx", + "workspaceContains:*.cake", "workspaceContains:**/*.csproj", "workspaceContains:**/*.sln", - "workspaceContains:**/*.csx" + "workspaceContains:**/*.csx", + "workspaceContains:**/*.cake" ], "contributes": { "configuration": { "title": "C# configuration", "properties": { + "csharp.format.enable": { + "type": "boolean", + "default": true, + "description": "Enable/disable default C# formatter (requires restart)." + }, "csharp.suppressDotnetInstallWarning": { "type": "boolean", "default": false, @@ -316,6 +324,16 @@ "default": true, "description": "Suppress 'hidden' diagnostics (such as 'unnecessary using directives') from appearing in the editor or the Problems pane." }, + "csharp.referencesCodeLens.enabled": { + "type": "boolean", + "default": true, + "description": "Specifies whether the references CodeLens should be show be shown." + }, + "csharp.testsCodeLens.enabled": { + "type": "boolean", + "default": true, + "description": "Specifies whether the run and debug test CodeLens should be show be shown." + }, "omnisharp.path": { "type": [ "string", @@ -456,13 +474,13 @@ "properties": { "program": { "type": "string", - "description": "Path to the application dll or .NET Core host executable to launch.\nThis property normally takes the form: '${workspaceRoot}/bin/Debug/(target-framework)/(project-name.dll)'\nExample: '${workspaceRoot}/bin/Debug/netcoreapp1.1/MyProject.dll'\n\nWhere:\n(target-framework) is the framework that the debugged project is being built for. This is normally found in the project file as the 'TargetFramework' property.\n(project-name.dll) is the name of debugged project's build output dll. This is normally the same as the project file name but with a '.dll' extension.", - "default": "${workspaceRoot}/bin/Debug//.dll" + "description": "Path to the application dll or .NET Core host executable to launch.\nThis property normally takes the form: '${workspaceFolder}/bin/Debug/(target-framework)/(project-name.dll)'\nExample: '${workspaceFolder}/bin/Debug/netcoreapp1.1/MyProject.dll'\n\nWhere:\n(target-framework) is the framework that the debugged project is being built for. This is normally found in the project file as the 'TargetFramework' property.\n(project-name.dll) is the name of debugged project's build output dll. This is normally the same as the project file name but with a '.dll' extension.", + "default": "${workspaceFolder}/bin/Debug//.dll" }, "cwd": { "type": "string", "description": "Path to the working directory of the program being debugged. Default is the current workspace.", - "default": "${workspaceRoot}" + "default": "${workspaceFolder}" }, "args": { "anyOf": [ @@ -688,7 +706,7 @@ "debuggerPath" ], "default": { - "pipeCwd": "${workspaceRoot}", + "pipeCwd": "${workspaceFolder}", "pipeProgram": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'", "pipeArgs": [], "debuggerPath": "enter the path for the debugger on the target machine, for example ~/vsdbg/vsdbg" @@ -697,7 +715,7 @@ "pipeCwd": { "type": "string", "description": "The fully qualified path to the working directory for the pipe program.", - "default": "${workspaceRoot}" + "default": "${workspaceFolder}" }, "pipeProgram": { "type": "string", @@ -743,7 +761,7 @@ "windows": { "description": "Windows-specific pipe launch configuration options", "default": { - "pipeCwd": "${workspaceRoot}", + "pipeCwd": "${workspaceFolder}", "pipeProgram": "enter the fully qualified path for the pipe program name, for example 'c:\\tools\\plink.exe'", "pipeArgs": [] }, @@ -752,7 +770,7 @@ "pipeCwd": { "type": "string", "description": "The fully qualified path to the working directory for the pipe program.", - "default": "${workspaceRoot}" + "default": "${workspaceFolder}" }, "pipeProgram": { "type": "string", @@ -795,7 +813,7 @@ "osx": { "description": "OSX-specific pipe launch configuration options", "default": { - "pipeCwd": "${workspaceRoot}", + "pipeCwd": "${workspaceFolder}", "pipeProgram": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'", "pipeArgs": [] }, @@ -804,7 +822,7 @@ "pipeCwd": { "type": "string", "description": "The fully qualified path to the working directory for the pipe program.", - "default": "${workspaceRoot}" + "default": "${workspaceFolder}" }, "pipeProgram": { "type": "string", @@ -847,7 +865,7 @@ "linux": { "description": "Linux-specific pipe launch configuration options", "default": { - "pipeCwd": "${workspaceRoot}", + "pipeCwd": "${workspaceFolder}", "pipeProgram": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'", "pipeArgs": [] }, @@ -856,7 +874,7 @@ "pipeCwd": { "type": "string", "description": "The fully qualified path to the working directory for the pipe program.", - "default": "${workspaceRoot}" + "default": "${workspaceFolder}" }, "pipeProgram": { "type": "string", @@ -996,7 +1014,7 @@ "debuggerPath" ], "default": { - "pipeCwd": "${workspaceRoot}", + "pipeCwd": "${workspaceFolder}", "pipeProgram": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'", "pipeArgs": [], "debuggerPath": "enter the path for the debugger on the target machine, for example ~/vsdbg/vsdbg" @@ -1005,7 +1023,7 @@ "pipeCwd": { "type": "string", "description": "The fully qualified path to the working directory for the pipe program.", - "default": "${workspaceRoot}" + "default": "${workspaceFolder}" }, "pipeProgram": { "type": "string", @@ -1051,7 +1069,7 @@ "windows": { "description": "Windows-specific pipe launch configuration options", "default": { - "pipeCwd": "${workspaceRoot}", + "pipeCwd": "${workspaceFolder}", "pipeProgram": "enter the fully qualified path for the pipe program name, for example 'c:\\tools\\plink.exe'", "pipeArgs": [] }, @@ -1060,7 +1078,7 @@ "pipeCwd": { "type": "string", "description": "The fully qualified path to the working directory for the pipe program.", - "default": "${workspaceRoot}" + "default": "${workspaceFolder}" }, "pipeProgram": { "type": "string", @@ -1103,7 +1121,7 @@ "osx": { "description": "OSX-specific pipe launch configuration options", "default": { - "pipeCwd": "${workspaceRoot}", + "pipeCwd": "${workspaceFolder}", "pipeProgram": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'", "pipeArgs": [] }, @@ -1112,7 +1130,7 @@ "pipeCwd": { "type": "string", "description": "The fully qualified path to the working directory for the pipe program.", - "default": "${workspaceRoot}" + "default": "${workspaceFolder}" }, "pipeProgram": { "type": "string", @@ -1155,7 +1173,7 @@ "linux": { "description": "Linux-specific pipe launch configuration options", "default": { - "pipeCwd": "${workspaceRoot}", + "pipeCwd": "${workspaceFolder}", "pipeProgram": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'", "pipeArgs": [] }, @@ -1164,7 +1182,7 @@ "pipeCwd": { "type": "string", "description": "The fully qualified path to the working directory for the pipe program.", - "default": "${workspaceRoot}" + "default": "${workspaceFolder}" }, "pipeProgram": { "type": "string", @@ -1306,55 +1324,6 @@ } } } - ], - "initialConfigurations": [ - { - "name": ".NET Core Launch (console)", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - "program": "${workspaceRoot}/bin/Debug//.dll", - "args": [], - "cwd": "${workspaceRoot}", - "stopAtEntry": false, - "console": "internalConsole" - }, - { - "name": ".NET Core Launch (web)", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - "program": "${workspaceRoot}/bin/Debug//.dll", - "args": [], - "cwd": "${workspaceRoot}", - "stopAtEntry": false, - "launchBrowser": { - "enabled": true, - "args": "${auto-detect-url}", - "windows": { - "command": "cmd.exe", - "args": "/C start ${auto-detect-url}" - }, - "osx": { - "command": "open" - }, - "linux": { - "command": "xdg-open" - } - }, - "env": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "sourceFileMap": { - "/Views": "${workspaceRoot}/Views" - } - }, - { - "name": ".NET Core Attach", - "type": "coreclr", - "request": "attach", - "processId": "${command:pickProcess}" - } ] }, { @@ -1383,13 +1352,13 @@ "properties": { "program": { "type": "string", - "description": "Path to the application dll or .NET Core host executable to launch.\nThis property normally takes the form: '${workspaceRoot}/bin/Debug/(target-framework)/(project-name.dll)'\nExample: '${workspaceRoot}/bin/Debug/netcoreapp1.1/MyProject.dll'\n\nWhere:\n(target-framework) is the framework that the debugged project is being built for. This is normally found in the project file as the 'TargetFramework' property.\n(project-name.dll) is the name of debugged project's build output dll. This is normally the same as the project file name but with a '.dll' extension.", - "default": "${workspaceRoot}/bin/Debug//.dll" + "description": "Path to the application dll or .NET Core host executable to launch.\nThis property normally takes the form: '${workspaceFolder}/bin/Debug/(target-framework)/(project-name.dll)'\nExample: '${workspaceFolder}/bin/Debug/netcoreapp1.1/MyProject.dll'\n\nWhere:\n(target-framework) is the framework that the debugged project is being built for. This is normally found in the project file as the 'TargetFramework' property.\n(project-name.dll) is the name of debugged project's build output dll. This is normally the same as the project file name but with a '.dll' extension.", + "default": "${workspaceFolder}/bin/Debug//.dll" }, "cwd": { "type": "string", "description": "Path to the working directory of the program being debugged. Default is the current workspace.", - "default": "${workspaceRoot}" + "default": "${workspaceFolder}" }, "args": { "anyOf": [ @@ -1615,7 +1584,7 @@ "debuggerPath" ], "default": { - "pipeCwd": "${workspaceRoot}", + "pipeCwd": "${workspaceFolder}", "pipeProgram": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'", "pipeArgs": [], "debuggerPath": "enter the path for the debugger on the target machine, for example ~/vsdbg/vsdbg" @@ -1624,7 +1593,7 @@ "pipeCwd": { "type": "string", "description": "The fully qualified path to the working directory for the pipe program.", - "default": "${workspaceRoot}" + "default": "${workspaceFolder}" }, "pipeProgram": { "type": "string", @@ -1670,7 +1639,7 @@ "windows": { "description": "Windows-specific pipe launch configuration options", "default": { - "pipeCwd": "${workspaceRoot}", + "pipeCwd": "${workspaceFolder}", "pipeProgram": "enter the fully qualified path for the pipe program name, for example 'c:\\tools\\plink.exe'", "pipeArgs": [] }, @@ -1679,7 +1648,7 @@ "pipeCwd": { "type": "string", "description": "The fully qualified path to the working directory for the pipe program.", - "default": "${workspaceRoot}" + "default": "${workspaceFolder}" }, "pipeProgram": { "type": "string", @@ -1722,7 +1691,7 @@ "osx": { "description": "OSX-specific pipe launch configuration options", "default": { - "pipeCwd": "${workspaceRoot}", + "pipeCwd": "${workspaceFolder}", "pipeProgram": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'", "pipeArgs": [] }, @@ -1731,7 +1700,7 @@ "pipeCwd": { "type": "string", "description": "The fully qualified path to the working directory for the pipe program.", - "default": "${workspaceRoot}" + "default": "${workspaceFolder}" }, "pipeProgram": { "type": "string", @@ -1774,7 +1743,7 @@ "linux": { "description": "Linux-specific pipe launch configuration options", "default": { - "pipeCwd": "${workspaceRoot}", + "pipeCwd": "${workspaceFolder}", "pipeProgram": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'", "pipeArgs": [] }, @@ -1783,7 +1752,7 @@ "pipeCwd": { "type": "string", "description": "The fully qualified path to the working directory for the pipe program.", - "default": "${workspaceRoot}" + "default": "${workspaceFolder}" }, "pipeProgram": { "type": "string", @@ -1923,7 +1892,7 @@ "debuggerPath" ], "default": { - "pipeCwd": "${workspaceRoot}", + "pipeCwd": "${workspaceFolder}", "pipeProgram": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'", "pipeArgs": [], "debuggerPath": "enter the path for the debugger on the target machine, for example ~/vsdbg/vsdbg" @@ -1932,7 +1901,7 @@ "pipeCwd": { "type": "string", "description": "The fully qualified path to the working directory for the pipe program.", - "default": "${workspaceRoot}" + "default": "${workspaceFolder}" }, "pipeProgram": { "type": "string", @@ -1978,7 +1947,7 @@ "windows": { "description": "Windows-specific pipe launch configuration options", "default": { - "pipeCwd": "${workspaceRoot}", + "pipeCwd": "${workspaceFolder}", "pipeProgram": "enter the fully qualified path for the pipe program name, for example 'c:\\tools\\plink.exe'", "pipeArgs": [] }, @@ -1987,7 +1956,7 @@ "pipeCwd": { "type": "string", "description": "The fully qualified path to the working directory for the pipe program.", - "default": "${workspaceRoot}" + "default": "${workspaceFolder}" }, "pipeProgram": { "type": "string", @@ -2030,7 +1999,7 @@ "osx": { "description": "OSX-specific pipe launch configuration options", "default": { - "pipeCwd": "${workspaceRoot}", + "pipeCwd": "${workspaceFolder}", "pipeProgram": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'", "pipeArgs": [] }, @@ -2039,7 +2008,7 @@ "pipeCwd": { "type": "string", "description": "The fully qualified path to the working directory for the pipe program.", - "default": "${workspaceRoot}" + "default": "${workspaceFolder}" }, "pipeProgram": { "type": "string", @@ -2082,7 +2051,7 @@ "linux": { "description": "Linux-specific pipe launch configuration options", "default": { - "pipeCwd": "${workspaceRoot}", + "pipeCwd": "${workspaceFolder}", "pipeProgram": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'", "pipeArgs": [] }, @@ -2091,7 +2060,7 @@ "pipeCwd": { "type": "string", "description": "The fully qualified path to the working directory for the pipe program.", - "default": "${workspaceRoot}" + "default": "${workspaceFolder}" }, "pipeProgram": { "type": "string", diff --git a/src/CSharpExtDownloader.ts b/src/CSharpExtDownloader.ts index 2eb91096fb..0002961055 100644 --- a/src/CSharpExtDownloader.ts +++ b/src/CSharpExtDownloader.ts @@ -115,7 +115,7 @@ export class CSharpExtDownloader telemetryProps['platform.architecture'] = platformInfo.architecture; telemetryProps['platform.platform'] = platformInfo.platform; if (platformInfo.distribution) { - telemetryProps['platform.distribution'] = platformInfo.distribution.toString(); + telemetryProps['platform.distribution'] = platformInfo.distribution.toTelemetryString(); } if (this.reporter) { diff --git a/src/assets.ts b/src/assets.ts index b439e01539..4a26cc8f0a 100644 --- a/src/assets.ts +++ b/src/assets.ts @@ -46,7 +46,7 @@ export class AssetGenerator { // First, we'll check for .NET Core .csproj projects. if (workspaceInfo.MsBuild && workspaceInfo.MsBuild.Projects) { - const executableMSBuildProjects = findExecutableMSBuildProjects(workspaceInfo.MsBuild.Projects); + const executableMSBuildProjects = protocol.findExecutableMSBuildProjects(workspaceInfo.MsBuild.Projects); const targetMSBuildProject = executableMSBuildProjects.length > 0 ? executableMSBuildProjects[0] @@ -64,7 +64,7 @@ export class AssetGenerator { } // Next, we'll try looking for project.json projects. - const executableProjects = findExecutableProjectJsonProjects(workspaceInfo.DotNet.Projects, configurationName); + const executableProjects = protocol.findExecutableProjectJsonProjects(workspaceInfo.DotNet.Projects, configurationName); // TODO: We arbitrarily pick the first executable project that we find. This will need // revisiting when we project a "start up project" selector. @@ -126,10 +126,10 @@ export class AssetGenerator { private computeProgramPath() { if (!this.hasProject) { // If there's no target project data, use a placeholder for the path. - return '${workspaceRoot}/bin/Debug//.dll'; + return '${workspaceFolder}/bin/Debug//.dll'; } - let result = '${workspaceRoot}'; + let result = '${workspaceFolder}'; if (this.projectPath) { result = path.join(result, path.relative(this.rootPath, this.projectPath)); @@ -143,10 +143,10 @@ export class AssetGenerator { private computeWorkingDirectory() : string { if (!this.hasProject) { // If there's no target project data, use a placeholder for the path. - return '${workspaceRoot}'; + return '${workspaceFolder}'; } - let result = '${workspaceRoot}'; + let result = '${workspaceFolder}'; if (this.projectPath) { result = path.join(result, path.relative(this.rootPath, this.projectPath)); @@ -155,75 +155,10 @@ export class AssetGenerator { return result; } - private createLaunchConfiguration(): string{ - return ` -{ - "name": ".NET Core Launch (console)", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - // If you have changed target frameworks, make sure to update the program path. - "program": "${util.convertNativePathToPosix(this.computeProgramPath())}", - "args": [], - "cwd": "${util.convertNativePathToPosix(this.computeWorkingDirectory())}", - // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window - "console": "internalConsole", - "stopAtEntry": false, - "internalConsoleOptions": "openOnSessionStart" -}`; - } - - private createWebLaunchConfiguration(): string { - return ` -{ - "name": ".NET Core Launch (web)", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - // If you have changed target frameworks, make sure to update the program path. - "program": "${util.convertNativePathToPosix(this.computeProgramPath())}", - "args": [], - "cwd": "${util.convertNativePathToPosix(this.computeWorkingDirectory())}", - "stopAtEntry": false, - "internalConsoleOptions": "openOnSessionStart", - "launchBrowser": { - "enabled": true, - "args": "\${auto-detect-url}", - "windows": { - "command": "cmd.exe", - "args": "/C start \${auto-detect-url}" - }, - "osx": { - "command": "open" - }, - "linux": { - "command": "xdg-open" - } - }, - "env": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "sourceFileMap": { - "/Views": "\${workspaceRoot}/Views" - } -}`; - } - - // AttachConfiguration - private createAttachConfiguration(): string { - return ` -{ - "name": ".NET Core Attach", - "type": "coreclr", - "request": "attach", - "processId": "\${command:pickProcess}" -}`; - } - public createLaunchJson(isWebProject: boolean): string { if (!isWebProject) { - const launchConfigurationsMassaged: string = indentJsonString(this.createLaunchConfiguration()); - const attachConfigurationsMassaged: string = indentJsonString(this.createAttachConfiguration()); + const launchConfigurationsMassaged: string = indentJsonString(createLaunchConfiguration(this.computeProgramPath(), this.computeWorkingDirectory())); + const attachConfigurationsMassaged: string = indentJsonString(createAttachConfiguration()); return ` [ ${launchConfigurationsMassaged}, @@ -231,8 +166,8 @@ export class AssetGenerator { ]`; } else { - const webLaunchConfigurationsMassaged: string = indentJsonString(this.createWebLaunchConfiguration()); - const attachConfigurationsMassaged: string = indentJsonString(this.createAttachConfiguration()); + const webLaunchConfigurationsMassaged: string = indentJsonString(createWebLaunchConfiguration(this.computeProgramPath(), this.computeWorkingDirectory())); + const attachConfigurationsMassaged: string = indentJsonString(createAttachConfiguration()); return ` [ ${webLaunchConfigurationsMassaged}, @@ -244,69 +179,91 @@ export class AssetGenerator { private createBuildTaskDescription(): tasks.TaskDescription { let buildPath = ''; if (this.hasProject) { - buildPath = path.join('${workspaceRoot}', path.relative(this.rootPath, this.projectFilePath)); + buildPath = path.join('${workspaceFolder}', path.relative(this.rootPath, this.projectFilePath)); } return { taskName: 'build', - args: [util.convertNativePathToPosix(buildPath)], - isBuildCommand: true, + command: 'dotnet', + type: 'process', + args: ['build', util.convertNativePathToPosix(buildPath)], problemMatcher: '$msCompile' }; } public createTasksConfiguration(): tasks.TaskConfiguration { return { - version: '0.1.0', - command: 'dotnet', - isShellCommand: true, - args: [], + version: "2.0.0", tasks: [this.createBuildTaskDescription()] }; } } -function findExecutableMSBuildProjects(projects: protocol.MSBuildProject[]) { - let result: protocol.MSBuildProject[] = []; - - projects.forEach(project => { - if (project.IsExe && protocol.findNetCoreAppTargetFramework(project) !== undefined) { - result.push(project); +export function createWebLaunchConfiguration(programPath: string, workingDirectory: string): string { + return ` +{ + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${util.convertNativePathToPosix(programPath)}", + "args": [], + "cwd": "${util.convertNativePathToPosix(workingDirectory)}", + "stopAtEntry": false, + "internalConsoleOptions": "openOnSessionStart", + "launchBrowser": { + "enabled": true, + "args": "\${auto-detect-url}", + "windows": { + "command": "cmd.exe", + "args": "/C start \${auto-detect-url}" + }, + "osx": { + "command": "open" + }, + "linux": { + "command": "xdg-open" } - }); - - return result; + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "\${workspaceFolder}/Views" + } +}`; } -function findExecutableProjectJsonProjects(projects: protocol.DotNetProject[], configurationName: string) { - let result: protocol.DotNetProject[] = []; - - projects.forEach(project => { - project.Configurations.forEach(configuration => { - if (configuration.Name === configurationName && configuration.EmitEntryPoint === true) { - if (project.Frameworks.length > 0) { - result.push(project); - } - } - }); - }); - - return result; + export function createLaunchConfiguration(programPath: string, workingDirectory: string): string { + return ` +{ + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${util.convertNativePathToPosix(programPath)}", + "args": [], + "cwd": "${util.convertNativePathToPosix(workingDirectory)}", + // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window + "console": "internalConsole", + "stopAtEntry": false, + "internalConsoleOptions": "openOnSessionStart" +}`; } -function containsDotNetCoreProjects(workspaceInfo: protocol.WorkspaceInformationResponse) { - if (workspaceInfo.DotNet && findExecutableProjectJsonProjects(workspaceInfo.DotNet.Projects, 'Debug').length > 0) { - return true; - } - - if (workspaceInfo.MsBuild && findExecutableMSBuildProjects(workspaceInfo.MsBuild.Projects).length > 0) { - return true; - } - - return false; +// AttachConfiguration +export function createAttachConfiguration(): string { + return ` +{ + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "\${command:pickProcess}" +}`; } - interface Operations { addTasksJson?: boolean; updateTasksJson?: boolean; @@ -322,30 +279,44 @@ function getOperations(generator: AssetGenerator) { getLaunchOperations(generator.launchJsonPath, operations)); } +/** + * Will return old (version=0.1.0) or new (version=2.0.0) tasks. If there are any of them, do not + * write over the tasks.json. + */ function getBuildTasks(tasksConfiguration: tasks.TaskConfiguration): tasks.TaskDescription[] { let result: tasks.TaskDescription[] = []; - function findBuildTask(tasksDescriptions: tasks.TaskDescription[]) { - if (tasksDescriptions) { - const buildTask = tasksDescriptions.find(td => td.isBuildCommand); - if (buildTask !== undefined) { - result.push(buildTask); - } + const tasksV1: string = "0.1.0"; + const tasksV2: string = "2.0.0"; + + function findBuildTask(version: string, tasksDescriptions: tasks.TaskDescription[]) { + let buildTask = undefined; + // Find the old tasks + if (version === tasksV1 && tasksDescriptions) { + buildTask = tasksDescriptions.find(td => td.isBuildCommand); + } + // Find the new tasks + else if (version === tasksV2 && tasksDescriptions) { + buildTask = tasksDescriptions.find(td => td.group === 'build'); + } + + if (buildTask !== undefined) { + result.push(buildTask); } } - findBuildTask(tasksConfiguration.tasks); + findBuildTask(tasksConfiguration.version, tasksConfiguration.tasks); if (tasksConfiguration.windows) { - findBuildTask(tasksConfiguration.windows.tasks); + findBuildTask(tasksConfiguration.version, tasksConfiguration.windows.tasks); } if (tasksConfiguration.osx) { - findBuildTask(tasksConfiguration.osx.tasks); + findBuildTask(tasksConfiguration.version, tasksConfiguration.osx.tasks); } if (tasksConfiguration.linux) { - findBuildTask(tasksConfiguration.linux.tasks); + findBuildTask(tasksConfiguration.version, tasksConfiguration.linux.tasks); } return result; @@ -421,7 +392,7 @@ function promptToAddAssets() { }); } -function addTasksJsonIfNecessary(generator: AssetGenerator, operations: Operations) { +export function addTasksJsonIfNecessary(generator: AssetGenerator, operations: Operations) { return new Promise((resolve, reject) => { if (!operations.addTasksJson) { return resolve(); @@ -498,7 +469,7 @@ export function addAssetsIfNecessary(server: OmniSharpServer): Promise { // If there are no .NET Core projects, we won't bother offering to add assets. - if (containsDotNetCoreProjects(info)) { + if (protocol.containsDotNetCoreProjects(info)) { const generator = new AssetGenerator(info); return getOperations(generator).then(operations => { if (!hasAddOperations(operations)) { @@ -577,7 +548,7 @@ function shouldGenerateAssets(generator: AssetGenerator) { export function generateAssets(server: OmniSharpServer) { serverUtils.requestWorkspaceInformation(server).then(info => { - if (containsDotNetCoreProjects(info)) { + if (protocol.containsDotNetCoreProjects(info)) { const generator = new AssetGenerator(info); getOperations(generator).then(operations => { if (hasAddOperations(operations)) { diff --git a/src/common.ts b/src/common.ts index 112affe215..8a31a6d161 100644 --- a/src/common.ts +++ b/src/common.ts @@ -167,4 +167,20 @@ export function deleteInstallFile(type: InstallFileType): Promise { export function convertNativePathToPosix(pathString: string): string { let parts = pathString.split(path.sep); return parts.join(path.posix.sep); +} + +/** + * This function checks to see if a subfolder is part of folder. + * + * Assumes subfolder and folder are absolute paths and have consistent casing. + * + * @param subfolder subfolder to check if it is part of the folder parameter + * @param folder folder to check aganist + */ +export function isSubfolderOf(subfolder: string, folder: string): boolean { + const subfolderArray: string[] = subfolder.split(path.sep); + const folderArray: string[] = folder.split(path.sep); + + // Check to see that every sub directory in subfolder exists in folder. + return subfolderArray.length <= folderArray.length && subfolderArray.every((subpath, index) => folderArray[index] === subpath); } \ No newline at end of file diff --git a/src/configurationProvider.ts b/src/configurationProvider.ts new file mode 100644 index 0000000000..d4dcdea569 --- /dev/null +++ b/src/configurationProvider.ts @@ -0,0 +1,108 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { parse } from 'jsonc-parser'; +import { OmniSharpServer } from './omnisharp/server'; +import * as serverUtils from './omnisharp/utils'; +import { containsDotNetCoreProjects } from './omnisharp/protocol'; +import { AssetGenerator, addTasksJsonIfNecessary, createLaunchConfiguration, createAttachConfiguration, createWebLaunchConfiguration } from './assets'; +import { isSubfolderOf } from './common'; + +export class CSharpConfigurationProvider implements vscode.DebugConfigurationProvider { + private server: OmniSharpServer; + + public constructor(server: OmniSharpServer) { + this.server = server; + } + + /** + * TODO: Remove function when https://github.com/OmniSharp/omnisharp-roslyn/issues/909 is resolved. + * + * Note: serverUtils.requestWorkspaceInformation only retrieves one folder for multi-root workspaces. Therefore, generator will be incorrect for all folders + * except the first in a workspace. Currently, this only works if the requested folder is the same as the server's solution path or folder. + */ + private checkWorkspaceInformationMatchesWorkspaceFolder(folder: vscode.WorkspaceFolder | undefined): Promise { + const solutionPathOrFolder: string = this.server.getSolutionPathOrFolder(); + + // Make sure folder, folder.uri, and solutionPathOrFolder are defined. + if (!folder || !folder.uri || !solutionPathOrFolder) + { + return Promise.resolve(false); + } + + let serverFolder = solutionPathOrFolder; + // If its a .sln file, get the folder of the solution. + return fs.lstat(solutionPathOrFolder).then(stat => { + return stat.isFile(); + }).then(isFile => { + if (isFile) + { + serverFolder = path.dirname(solutionPathOrFolder); + } + + // Get absolute paths of current folder and server folder. + const currentFolder = path.resolve(folder.uri.fsPath); + serverFolder = path.resolve(serverFolder); + + return currentFolder && folder.uri && isSubfolderOf(serverFolder, currentFolder); + }); + } + + /** + * Returns a list of initial debug configurations based on contextual information, e.g. package.json or folder. + */ + provideDebugConfigurations(folder: vscode.WorkspaceFolder | undefined, token?: vscode.CancellationToken): vscode.ProviderResult { + return serverUtils.requestWorkspaceInformation(this.server).then(info => { + return this.checkWorkspaceInformationMatchesWorkspaceFolder(folder).then(workspaceMatches => { + const generator = new AssetGenerator(info); + if (workspaceMatches && containsDotNetCoreProjects(info)) { + const dotVscodeFolder: string = path.join(folder.uri.fsPath, '.vscode'); + const tasksJsonPath: string = path.join(dotVscodeFolder, 'tasks.json'); + + // Make sure .vscode folder exists, addTasksJsonIfNecessary will fail to create tasks.json if the folder does not exist. + return fs.ensureDir(dotVscodeFolder).then(() => { + // Check to see if tasks.json exists. + return fs.pathExists(tasksJsonPath); + }).then(tasksJsonExists => { + // Enable addTasksJson if it does not exist. + return addTasksJsonIfNecessary(generator, {addTasksJson: !tasksJsonExists}); + }).then(() => { + const isWebProject = generator.hasWebServerDependency(); + const launchJson: string = generator.createLaunchJson(isWebProject); + + // jsonc-parser's parse function parses a JSON string with comments into a JSON object. However, this removes the comments. + return parse(launchJson); + }); + } + + // Error to be caught in the .catch() below to write default C# configurations + throw new Error("Does not contain .NET Core projects."); + }); + }).catch((err) => { + // Provider will always create an launch.json file. Providing default C# configurations. + // jsonc-parser's parse to convert to JSON object without comments. + return [ + parse(createLaunchConfiguration( + "${workspaceFolder}/bin/Debug//.dll", + '${workspaceFolder}')), + parse(createWebLaunchConfiguration( + "${workspaceFolder}/bin/Debug//.dll", + '${workspaceFolder}')), + parse(createAttachConfiguration()) + ]; + }); + } + + /** + * Try to add all missing attributes to the debug configuration being launched. + */ + resolveDebugConfiguration(folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration, token?: vscode.CancellationToken): vscode.ProviderResult { + // vsdbg does the error checking + return config; + } +} \ No newline at end of file diff --git a/src/features/changeForwarding.ts b/src/features/changeForwarding.ts index 45c0116755..798c037dd1 100644 --- a/src/features/changeForwarding.ts +++ b/src/features/changeForwarding.ts @@ -8,6 +8,7 @@ import {Disposable, Uri, workspace} from 'vscode'; import {OmniSharpServer} from '../omnisharp/server'; import * as serverUtils from '../omnisharp/utils'; +import { FileChangeType } from '../omnisharp/protocol'; function forwardDocumentChanges(server: OmniSharpServer): Disposable { @@ -31,23 +32,26 @@ function forwardDocumentChanges(server: OmniSharpServer): Disposable { function forwardFileChanges(server: OmniSharpServer): Disposable { - function onFileSystemEvent(uri: Uri): void { - if (!server.isRunning()) { - return; - } - - let req = { FileName: uri.fsPath }; - - serverUtils.filesChanged(server, [req]).catch(err => { - console.warn(`[o] failed to forward file change event for ${uri.fsPath}`, err); - return err; - }); + function onFileSystemEvent(changeType: FileChangeType): (Uri) => void { + return function(uri: Uri) + { + if (!server.isRunning()) { + return; + } + + let req = { FileName: uri.fsPath, changeType}; + + serverUtils.filesChanged(server, [req]).catch(err => { + console.warn(`[o] failed to forward file change event for ${uri.fsPath}`, err); + return err; + }); + }; } const watcher = workspace.createFileSystemWatcher('**/*.*'); - let d1 = watcher.onDidCreate(onFileSystemEvent); - let d2 = watcher.onDidChange(onFileSystemEvent); - let d3 = watcher.onDidDelete(onFileSystemEvent); + let d1 = watcher.onDidCreate(onFileSystemEvent(FileChangeType.Create)); + let d2 = watcher.onDidDelete(onFileSystemEvent(FileChangeType.Delete)); + let d3 = watcher.onDidChange(onFileSystemEvent(FileChangeType.Change)); return Disposable.from(watcher, d1, d2, d3); } diff --git a/src/features/codeActionProvider.ts b/src/features/codeActionProvider.ts index f48803e419..e7080f11ce 100644 --- a/src/features/codeActionProvider.ts +++ b/src/features/codeActionProvider.ts @@ -11,11 +11,12 @@ import AbstractProvider from './abstractProvider'; import * as protocol from '../omnisharp/protocol'; import { toRange2 } from '../omnisharp/typeConvertion'; import * as serverUtils from '../omnisharp/utils'; +import { Options } from '../omnisharp/options'; import TelemetryReporter from 'vscode-extension-telemetry'; export default class CodeActionProvider extends AbstractProvider implements vscode.CodeActionProvider { - private _disabled: boolean; + private _options: Options; private _commandId: string; constructor(server: OmniSharpServer, reporter: TelemetryReporter) { @@ -23,20 +24,19 @@ export default class CodeActionProvider extends AbstractProvider implements vsco this._commandId = 'omnisharp.runCodeAction'; - this._checkOption(); + this._resetCachedOptions(); - let d1 = vscode.workspace.onDidChangeConfiguration(this._checkOption, this); + let d1 = vscode.workspace.onDidChangeConfiguration(this._resetCachedOptions, this); let d2 = vscode.commands.registerCommand(this._commandId, this._runCodeAction, this); this.addDisposables(d1, d2); } - private _checkOption(): void { - let value = vscode.workspace.getConfiguration().get('csharp.disableCodeActions', false); - this._disabled = value; + private _resetCachedOptions(): void { + this._options = Options.Read(); } public provideCodeActions(document: vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext, token: vscode.CancellationToken): Promise { - if (this._disabled) { + if (this._options.disableCodeActions) { return; } diff --git a/src/features/codeLensProvider.ts b/src/features/codeLensProvider.ts index 202259caff..85b9a13154 100644 --- a/src/features/codeLensProvider.ts +++ b/src/features/codeLensProvider.ts @@ -13,6 +13,7 @@ import { toRange, toLocation } from '../omnisharp/typeConvertion'; import AbstractProvider from './abstractProvider'; import * as protocol from '../omnisharp/protocol'; import * as serverUtils from '../omnisharp/utils'; +import { Options } from '../omnisharp/options'; class OmniSharpCodeLens extends vscode.CodeLens { @@ -27,12 +28,21 @@ class OmniSharpCodeLens extends vscode.CodeLens { export default class OmniSharpCodeLensProvider extends AbstractProvider implements vscode.CodeLensProvider { private _testManager: TestManager; + private _options: Options; constructor(server: OmniSharpServer, reporter: TelemetryReporter, testManager: TestManager) { super(server, reporter); this._testManager = testManager; + this._resetCachedOptions(); + + let configChangedDisposable = vscode.workspace.onDidChangeConfiguration(this._resetCachedOptions, this); + this.addDisposables(configChangedDisposable); + } + + private _resetCachedOptions(): void { + this._options = Options.Read(); } private static filteredSymbolNames: { [name: string]: boolean } = { @@ -43,6 +53,11 @@ export default class OmniSharpCodeLensProvider extends AbstractProvider implemen }; provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.CodeLens[] | Thenable { + if (!this._options.showReferencesCodeLens && !this._options.showTestsCodeLens) + { + return []; + } + return serverUtils.currentFileMembersAsTree(this._server, { FileName: document.fileName }, token).then(tree => { let ret: vscode.CodeLens[] = []; tree.TopLevelTypeDefinitions.forEach(node => this._convertQuickFix(ret, document.fileName, node)); @@ -57,13 +72,17 @@ export default class OmniSharpCodeLensProvider extends AbstractProvider implemen } let lens = new OmniSharpCodeLens(fileName, toRange(node.Location)); - bucket.push(lens); + if (this._options.showReferencesCodeLens) { + bucket.push(lens); + } for (let child of node.ChildNodes) { this._convertQuickFix(bucket, fileName, child); } - this._updateCodeLensForTest(bucket, fileName, node); + if (this._options.showTestsCodeLens) { + this._updateCodeLensForTest(bucket, fileName, node); + } } resolveCodeLens(codeLens: vscode.CodeLens, token: vscode.CancellationToken): Thenable { @@ -104,7 +123,7 @@ export default class OmniSharpCodeLensProvider extends AbstractProvider implemen if (testFeature) { // this test method has a test feature let testFrameworkName = 'xunit'; - if (testFeature.Name == 'NunitTestMethod') { + if (testFeature.Name == 'NUnitTestMethod') { testFrameworkName = 'nunit'; } else if (testFeature.Name == 'MSTestMethod') { @@ -115,11 +134,9 @@ export default class OmniSharpCodeLensProvider extends AbstractProvider implemen toRange(node.Location), { title: "run test", command: 'dotnet.test.run', arguments: [testFeature.Data, fileName, testFrameworkName] })); - if (this._server.isDebugEnable()) { - bucket.push(new vscode.CodeLens( - toRange(node.Location), - { title: "debug test", command: 'dotnet.test.debug', arguments: [testFeature.Data, fileName, testFrameworkName] })); - } + bucket.push(new vscode.CodeLens( + toRange(node.Location), + { title: "debug test", command: 'dotnet.test.debug', arguments: [testFeature.Data, fileName, testFrameworkName] })); } } } diff --git a/src/features/completionItemProvider.ts b/src/features/completionItemProvider.ts index c2187f6715..0119c2c384 100644 --- a/src/features/completionItemProvider.ts +++ b/src/features/completionItemProvider.ts @@ -10,7 +10,7 @@ import AbstractSupport from './abstractProvider'; import * as protocol from '../omnisharp/protocol'; import * as serverUtils from '../omnisharp/utils'; import {createRequest} from '../omnisharp/typeConvertion'; -import {CompletionItemProvider, CompletionItem, CompletionItemKind, CancellationToken, TextDocument, Range, Position} from 'vscode'; +import {CompletionItemProvider, CompletionItem, CompletionItemKind, CompletionContext, CompletionTriggerKind, CancellationToken, TextDocument, Range, Position, CompletionList} from 'vscode'; export default class OmniSharpCompletionItemProvider extends AbstractSupport implements CompletionItemProvider { @@ -25,7 +25,7 @@ export default class OmniSharpCompletionItemProvider extends AbstractSupport imp ';', '+', '-', '*', '/', '%', '&', '|', '^', '!', '~', '=', '<', '>', '?', '@', '#', '\'', '\"', '\\']; - public provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken): Promise { + public provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): Promise { let wordToComplete = ''; let range = document.getWordRangeAtPosition(position); @@ -38,6 +38,10 @@ export default class OmniSharpCompletionItemProvider extends AbstractSupport imp req.WantDocumentationForEveryCompletionResult = true; req.WantKind = true; req.WantReturnType = true; + if (context.triggerKind == CompletionTriggerKind.TriggerCharacter) + { + req.TriggerCharacter = context.triggerCharacter; + } return serverUtils.autoComplete(this._server, req).then(responses => { @@ -93,7 +97,9 @@ export default class OmniSharpCompletionItemProvider extends AbstractSupport imp result.push(suggestion); } - return result; + // for short completions (up to 1 character), treat the list as incomplete + // because the server has likely witheld some matches due to performance constraints + return new CompletionList(result, wordToComplete.length > 1 ? false : true); }); } } diff --git a/src/features/definitionMetadataDocumentProvider.ts b/src/features/definitionMetadataDocumentProvider.ts index 66b6e3a8d2..f9a337da12 100644 --- a/src/features/definitionMetadataDocumentProvider.ts +++ b/src/features/definitionMetadataDocumentProvider.ts @@ -23,24 +23,26 @@ export default class DefinitionMetadataDocumentProvider implements TextDocumentC } public addMetadataResponse(metadataResponse: MetadataResponse) : Uri { - const uri = this.createUri(metadataResponse); - + const uri = this.createUri(metadataResponse.SourceName); this._documents.set(uri.toString(), metadataResponse); return uri; } + public getExistingMetadataResponseUri(sourceName: string) : Uri { + return this.createUri(sourceName); + } + public register() : void { this._registration = workspace.registerTextDocumentContentProvider(this.scheme, this); } - public provideTextDocumentContent(uri : Uri) : string { + public provideTextDocumentContent(uri: Uri) : string { return this._documents.get(uri.toString()).Source; } - private createUri(metadataResponse: MetadataResponse) : Uri { + private createUri(sourceName: string) : Uri { return Uri.parse(this.scheme + "://" + - metadataResponse.SourceName.replace(/\\/g, "/") - .replace(/(.*)\/(.*)/g, "$1/[metadata] $2")); + sourceName.replace(/\\/g, "/").replace(/(.*)\/(.*)/g, "$1/[metadata] $2")); } } \ No newline at end of file diff --git a/src/features/definitionProvider.ts b/src/features/definitionProvider.ts index 4088420264..61514c3915 100644 --- a/src/features/definitionProvider.ts +++ b/src/features/definitionProvider.ts @@ -8,7 +8,7 @@ import AbstractSupport from './abstractProvider'; import {MetadataRequest, GoToDefinitionRequest, MetadataSource} from '../omnisharp/protocol'; import * as serverUtils from '../omnisharp/utils'; -import {createRequest, toLocation} from '../omnisharp/typeConvertion'; +import {createRequest, toLocation, toLocationFromUri} from '../omnisharp/typeConvertion'; import {Uri, TextDocument, Position, Location, CancellationToken, DefinitionProvider} from 'vscode'; import DefinitionMetadataDocumentProvider from './definitionMetadataDocumentProvider'; import TelemetryReporter from 'vscode-extension-telemetry'; @@ -29,11 +29,23 @@ export default class CSharpDefinitionProvider extends AbstractSupport implements return serverUtils.goToDefinition(this._server, req, token).then(gotoDefinitionResponse => { + // the defintion is in source if (gotoDefinitionResponse && gotoDefinitionResponse.FileName) { + + // if it is part of an already used metadata file, retrieve its uri instead of going to the physical file + if (gotoDefinitionResponse.FileName.startsWith("$metadata$")) { + const uri = this._definitionMetadataDocumentProvider.getExistingMetadataResponseUri(gotoDefinitionResponse.FileName); + return toLocationFromUri(uri, gotoDefinitionResponse); + } + + // if it is a normal source definition, convert the response to a location return toLocation(gotoDefinitionResponse); + + // the definition is in metadata } else if (gotoDefinitionResponse.MetadataSource) { const metadataSource: MetadataSource = gotoDefinitionResponse.MetadataSource; + // go to metadata endpoint for more information return serverUtils.getMetadata(this._server, { Timeout: 5000, AssemblyName: metadataSource.AssemblyName, diff --git a/src/features/dotnetTest.ts b/src/features/dotnetTest.ts index 0b9332dbdf..27a2c0bace 100644 --- a/src/features/dotnetTest.ts +++ b/src/features/dotnetTest.ts @@ -112,11 +112,12 @@ export default class TestManager extends AbstractProvider { vscode.workspace.saveAll(/*includeUntitled*/ false)); } - private _runTest(fileName: string, testMethod: string, testFrameworkName: string): Promise { + private _runTest(fileName: string, testMethod: string, testFrameworkName: string, targetFrameworkVersion: string): Promise { const request: protocol.V2.RunTestRequest = { FileName: fileName, MethodName: testMethod, - TestFrameworkName: testFrameworkName + TestFrameworkName: testFrameworkName, + TargetFrameworkVersion: targetFrameworkVersion }; return serverUtils.runTest(this._server, request) @@ -162,7 +163,23 @@ export default class TestManager extends AbstractProvider { this._saveDirtyFiles() .then(_ => this._recordRunRequest(testFrameworkName)) - .then(_ => this._runTest(fileName, testMethod, testFrameworkName)) + .then(_ => serverUtils.requestProjectInformation(this._server, { FileName: fileName })) + .then(projectInfo => + { + let targetFrameworkVersion: string; + + if (projectInfo.DotNetProject) { + targetFrameworkVersion = undefined; + } + else if (projectInfo.MsBuildProject) { + targetFrameworkVersion = projectInfo.MsBuildProject.TargetFramework; + } + else { + throw new Error('Expected project.json or .csproj project.'); + } + + return this._runTest(fileName, testMethod, testFrameworkName, targetFrameworkVersion); + }) .then(results => this._reportResults(results)) .then(() => listener.dispose()) .catch(reason => { @@ -198,7 +215,7 @@ export default class TestManager extends AbstractProvider { return result; } - private _getLaunchConfigurationForVSTest(fileName: string, testMethod: string, testFrameworkName: string, debugEventListener: DebugEventListener): Promise { + private _getLaunchConfigurationForVSTest(fileName: string, testMethod: string, testFrameworkName: string, targetFrameworkVersion: string, debugEventListener: DebugEventListener): Promise { const output = this._getOutputChannel(); // Listen for test messages while getting start info. @@ -209,7 +226,8 @@ export default class TestManager extends AbstractProvider { const request: protocol.V2.DebugTestGetStartInfoRequest = { FileName: fileName, MethodName: testMethod, - TestFrameworkName: testFrameworkName + TestFrameworkName: testFrameworkName, + TargetFrameworkVersion: targetFrameworkVersion }; return serverUtils.debugTestGetStartInfo(this._server, request) @@ -219,7 +237,7 @@ export default class TestManager extends AbstractProvider { }); } - private _getLaunchConfigurationForLegacy(fileName: string, testMethod: string, testFrameworkName: string): Promise { + private _getLaunchConfigurationForLegacy(fileName: string, testMethod: string, testFrameworkName: string, targetFrameworkVersion: string): Promise { const output = this._getOutputChannel(); // Listen for test messages while getting start info. @@ -230,7 +248,8 @@ export default class TestManager extends AbstractProvider { const request: protocol.V2.GetTestStartInfoRequest = { FileName: fileName, MethodName: testMethod, - TestFrameworkName: testFrameworkName + TestFrameworkName: testFrameworkName, + TargetFrameworkVersion: targetFrameworkVersion }; return serverUtils.getTestStartInfo(this._server, request) @@ -240,12 +259,12 @@ export default class TestManager extends AbstractProvider { }); } - private _getLaunchConfiguration(debugType: string, fileName: string, testMethod: string, testFrameworkName: string, debugEventListener: DebugEventListener): Promise { + private _getLaunchConfiguration(debugType: string, fileName: string, testMethod: string, testFrameworkName: string, targetFrameworkVersion: string, debugEventListener: DebugEventListener): Promise { switch (debugType) { case 'legacy': - return this._getLaunchConfigurationForLegacy(fileName, testMethod, testFrameworkName); + return this._getLaunchConfigurationForLegacy(fileName, testMethod, testFrameworkName, targetFrameworkVersion); case 'vstest': - return this._getLaunchConfigurationForVSTest(fileName, testMethod, testFrameworkName, debugEventListener); + return this._getLaunchConfigurationForVSTest(fileName, testMethod, testFrameworkName, targetFrameworkVersion, debugEventListener); default: throw new Error(`Unexpected debug type: ${debugType}`); @@ -257,6 +276,7 @@ export default class TestManager extends AbstractProvider { // using VS Test. These require a different level of communication. let debugType: string; let debugEventListener: DebugEventListener = null; + let targetFrameworkVersion: string; const output = this._getOutputChannel(); @@ -270,10 +290,12 @@ export default class TestManager extends AbstractProvider { .then(projectInfo => { if (projectInfo.DotNetProject) { debugType = 'legacy'; + targetFrameworkVersion = ''; return Promise.resolve(); } else if (projectInfo.MsBuildProject) { debugType = 'vstest'; + targetFrameworkVersion = projectInfo.MsBuildProject.TargetFramework; debugEventListener = new DebugEventListener(fileName, this._server, output); return debugEventListener.start(); } @@ -281,8 +303,11 @@ export default class TestManager extends AbstractProvider { throw new Error('Expected project.json or .csproj project.'); } }) - .then(() => this._getLaunchConfiguration(debugType, fileName, testMethod, testFrameworkName, debugEventListener)) - .then(config => vscode.commands.executeCommand('vscode.startDebug', config)) + .then(() => this._getLaunchConfiguration(debugType, fileName, testMethod, testFrameworkName, targetFrameworkVersion, debugEventListener)) + .then(config => { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(fileName)); + return vscode.debug.startDebugging(workspaceFolder, config); + }) .catch(reason => { vscode.window.showErrorMessage(`Failed to start debugger: ${reason}`); if (debugEventListener != null) { diff --git a/src/features/status.ts b/src/features/status.ts index 54b8255842..98a4a26441 100644 --- a/src/features/status.ts +++ b/src/features/status.ts @@ -27,7 +27,8 @@ let defaultSelector: vscode.DocumentSelector = [ { pattern: '**/project.json' }, // project.json-files OR { pattern: '**/*.sln' }, // any solution file OR { pattern: '**/*.csproj' }, // an csproj file - { pattern: '**/*.csx' } // C# script + { pattern: '**/*.csx' }, // C# script + { pattern: '**/*.cake' } // Cake script ]; class Status { diff --git a/src/omnisharp/extension.ts b/src/omnisharp/extension.ts index ee0a56534a..f415e637fb 100644 --- a/src/omnisharp/extension.ts +++ b/src/omnisharp/extension.ts @@ -30,6 +30,7 @@ import { Options } from './options'; import { addAssetsIfNecessary, AddAssetResult } from '../assets'; import { sum, safeLength } from '../common'; import * as utils from './utils'; +import { CSharpConfigurationProvider } from '../configurationProvider'; export function activate(context: vscode.ExtensionContext, reporter: TelemetryReporter, channel: vscode.OutputChannel) { const documentSelector: vscode.DocumentSelector = { @@ -37,6 +38,8 @@ export function activate(context: vscode.ExtensionContext, reporter: TelemetryRe scheme: 'file' // only files from disk }; + const options = Options.Read(); + const server = new OmniSharpServer(reporter); const advisor = new Advisor(server); // create before server is started const disposables: vscode.Disposable[] = []; @@ -60,9 +63,11 @@ export function activate(context: vscode.ExtensionContext, reporter: TelemetryRe localDisposables.push(vscode.languages.registerReferenceProvider(documentSelector, new ReferenceProvider(server, reporter))); localDisposables.push(vscode.languages.registerHoverProvider(documentSelector, new HoverProvider(server, reporter))); localDisposables.push(vscode.languages.registerRenameProvider(documentSelector, new RenameProvider(server, reporter))); - localDisposables.push(vscode.languages.registerDocumentRangeFormattingEditProvider(documentSelector, new FormatProvider(server, reporter))); - localDisposables.push(vscode.languages.registerOnTypeFormattingEditProvider(documentSelector, new FormatProvider(server, reporter), '}', ';')); - localDisposables.push(vscode.languages.registerCompletionItemProvider(documentSelector, new CompletionItemProvider(server, reporter), '.')); + if (options.useFormatting) { + localDisposables.push(vscode.languages.registerDocumentRangeFormattingEditProvider(documentSelector, new FormatProvider(server, reporter))); + localDisposables.push(vscode.languages.registerOnTypeFormattingEditProvider(documentSelector, new FormatProvider(server, reporter), '}', ';')); + } + localDisposables.push(vscode.languages.registerCompletionItemProvider(documentSelector, new CompletionItemProvider(server, reporter), '.', ' ')); localDisposables.push(vscode.languages.registerWorkspaceSymbolProvider(new WorkspaceSymbolProvider(server, reporter))); localDisposables.push(vscode.languages.registerSignatureHelpProvider(documentSelector, new SignatureHelpProvider(server, reporter), '(', ',')); const codeActionProvider = new CodeActionProvider(server, reporter); @@ -137,7 +142,6 @@ export function activate(context: vscode.ExtensionContext, reporter: TelemetryRe // read and store last solution or folder path disposables.push(server.onBeforeServerStart(path => context.workspaceState.update('lastSolutionPathOrFolder', path))); - const options = Options.Read(); if (options.autoStart) { server.autoStart(context.workspaceState.get('lastSolutionPathOrFolder')); } @@ -148,5 +152,8 @@ export function activate(context: vscode.ExtensionContext, reporter: TelemetryRe server.stop(); })); + // Register ConfigurationProvider + disposables.push(vscode.debug.registerDebugConfigurationProvider('coreclr', new CSharpConfigurationProvider(server))); + context.subscriptions.push(...disposables); } \ No newline at end of file diff --git a/src/omnisharp/launcher.ts b/src/omnisharp/launcher.ts index 48cd4829f5..2a70ac2737 100644 --- a/src/omnisharp/launcher.ts +++ b/src/omnisharp/launcher.ts @@ -17,7 +17,8 @@ export enum LaunchTargetKind { Solution, ProjectJson, Folder, - Csx + Csx, + Cake } /** @@ -38,26 +39,24 @@ export interface LaunchTarget { * is included if there are any `*.csproj` files present, but a `*.sln* file is not found. */ export function findLaunchTargets(): Thenable { - if (!vscode.workspace.rootPath) { + if (!vscode.workspace.workspaceFolders) { return Promise.resolve([]); } const options = Options.Read(); return vscode.workspace.findFiles( - /*include*/ '{**/*.sln,**/*.csproj,**/project.json,**/*.csx}', - /*exclude*/ '{**/node_modules/**,**/.git/**,**/bower_components/**}', - /*maxResults*/ options.maxProjectResults) - .then(resources => { - return select(resources, vscode.workspace.rootPath); - }); + /*include*/ '{**/*.sln,**/*.csproj,**/project.json,**/*.csx,**/*.cake}', + /*exclude*/ '{**/node_modules/**,**/.git/**,**/bower_components/**}', + /*maxResults*/ options.maxProjectResults) + .then(resourcesToLaunchTargets); } -function select(resources: vscode.Uri[], rootPath: string): LaunchTarget[] { +function resourcesToLaunchTargets(resources: vscode.Uri[]): LaunchTarget[] { // The list of launch targets is calculated like so: // * If there are .csproj files, .sln files are considered as launch targets. // * Any project.json file is considered a launch target. - // * If there is no project.json file in the root, the root as added as a launch target. + // * If there is no project.json file in a workspace folder, the workspace folder as added as a launch target. // * Additionally, if there are .csproj files, but no .sln file, the root is added as a launch target. // // TODO: @@ -69,74 +68,116 @@ function select(resources: vscode.Uri[], rootPath: string): LaunchTarget[] { return []; } - let targets: LaunchTarget[] = [], - hasCsProjFiles = false, - hasSlnFile = false, - hasProjectJson = false, - hasProjectJsonAtRoot = false, - hasCSX = false; + let workspaceFolderToUriMap = new Map(); + + for (let resource of resources) { + let folder = vscode.workspace.getWorkspaceFolder(resource); + if (folder) { + let buckets: vscode.Uri[]; - hasCsProjFiles = resources.some(isCSharpProject); + if (workspaceFolderToUriMap.has(folder.index)) { + buckets = workspaceFolderToUriMap.get(folder.index); + } else { + buckets = []; + workspaceFolderToUriMap.set(folder.index, buckets); + } - resources.forEach(resource => { - // Add .sln files if there are .csproj files - if (hasCsProjFiles && isSolution(resource)) { - hasSlnFile = true; + buckets.push(resource); + } + } + let targets: LaunchTarget[] = []; + + workspaceFolderToUriMap.forEach((resources, folderIndex) => + { + let hasCsProjFiles = false, + hasSlnFile = false, + hasProjectJson = false, + hasProjectJsonAtRoot = false, + hasCSX = false, + hasCake = false; + + hasCsProjFiles = resources.some(isCSharpProject); + + let folder = vscode.workspace.workspaceFolders[folderIndex]; + let folderPath = folder.uri.fsPath; + + resources.forEach(resource => { + // Add .sln files if there are .csproj files + if (hasCsProjFiles && isSolution(resource)) { + hasSlnFile = true; + + targets.push({ + label: path.basename(resource.fsPath), + description: vscode.workspace.asRelativePath(path.dirname(resource.fsPath)), + target: resource.fsPath, + directory: path.dirname(resource.fsPath), + kind: LaunchTargetKind.Solution + }); + } + + // Add project.json files + if (isProjectJson(resource)) { + const dirname = path.dirname(resource.fsPath); + hasProjectJson = true; + hasProjectJsonAtRoot = hasProjectJsonAtRoot || dirname === folderPath; + + targets.push({ + label: path.basename(resource.fsPath), + description: vscode.workspace.asRelativePath(path.dirname(resource.fsPath)), + target: dirname, + directory: dirname, + kind: LaunchTargetKind.ProjectJson + }); + } + + // Discover if there is any CSX file + if (!hasCSX && isCsx(resource)) { + hasCSX = true; + } + + // Discover if there is any Cake file + if (!hasCake && isCake(resource)) { + hasCake = true; + } + }); + + // Add the root folder under the following circumstances: + // * If there are .csproj files, but no .sln file, and none in the root. + // * If there are project.json files, but none in the root. + if ((hasCsProjFiles && !hasSlnFile) || (hasProjectJson && !hasProjectJsonAtRoot)) { targets.push({ - label: path.basename(resource.fsPath), - description: vscode.workspace.asRelativePath(path.dirname(resource.fsPath)), - target: resource.fsPath, - directory: path.dirname(resource.fsPath), - kind: LaunchTargetKind.Solution + label: path.basename(folderPath), + description: '', + target: folderPath, + directory: folderPath, + kind: LaunchTargetKind.Folder }); } - - // Add project.json files - if (isProjectJson(resource)) { - const dirname = path.dirname(resource.fsPath); - hasProjectJson = true; - hasProjectJsonAtRoot = hasProjectJsonAtRoot || dirname === rootPath; - + + // if we noticed any CSX file(s), add a single CSX-specific target pointing at the root folder + if (hasCSX) { targets.push({ - label: path.basename(resource.fsPath), - description: vscode.workspace.asRelativePath(path.dirname(resource.fsPath)), - target: dirname, - directory: dirname, - kind: LaunchTargetKind.ProjectJson + label: "CSX", + description: path.basename(folderPath), + target: folderPath, + directory: folderPath, + kind: LaunchTargetKind.Csx }); } - - // Discover if there is any CSX file - if (!hasCSX && isCsx(resource)) { - hasCSX = true; + + // if we noticed any Cake file(s), add a single Cake-specific target pointing at the root folder + if (hasCake) { + targets.push({ + label: "Cake", + description: path.basename(folderPath), + target: folderPath, + directory: folderPath, + kind: LaunchTargetKind.Cake + }); } }); - // Add the root folder under the following circumstances: - // * If there are .csproj files, but no .sln file, and none in the root. - // * If there are project.json files, but none in the root. - if ((hasCsProjFiles && !hasSlnFile) || (hasProjectJson && !hasProjectJsonAtRoot)) { - targets.push({ - label: path.basename(rootPath), - description: '', - target: rootPath, - directory: rootPath, - kind: LaunchTargetKind.Folder - }); - } - - // if we noticed any CSX file(s), add a single CSX-specific target pointing at the root folder - if (hasCSX) { - targets.push({ - label: "CSX", - description: path.basename(rootPath), - target: rootPath, - directory: rootPath, - kind: LaunchTargetKind.Csx - }); - } - return targets.sort((a, b) => a.directory.localeCompare(b.directory)); } @@ -156,6 +197,10 @@ function isCsx(resource: vscode.Uri): boolean { return /\.csx$/i.test(resource.fsPath); } +function isCake(resource: vscode.Uri): boolean { + return /\.cake$/i.test(resource.fsPath); +} + export interface LaunchResult { process: ChildProcess; command: string; @@ -175,7 +220,8 @@ export function launchOmniSharp(cwd: string, args: string[]): Promise reject(reason)); }); } @@ -193,18 +239,36 @@ function launch(cwd: string, args: string[]): Promise { args.push(`formattingOptions:indentationSize=${getConfigurationValue(globalConfig, csharpConfig, 'editor.tabSize', 4)}`); } - if (options.path && options.useMono) { - return launchNixMono(options.path, cwd, args); + // If the user has provide a path to OmniSharp, we'll use that. + if (options.path) { + if (platformInfo.isWindows()) { + return launchWindows(options.path, cwd, args); + } + + // If we're launching on macOS/Linux, we have two possibilities: + // 1. Launch using Mono + // 2. Launch process directly (e.g. a 'run' script) + return options.useMono + ? launchNixMono(options.path, cwd, args) + : launchNix(options.path, cwd, args); } - const launchPath = options.path || getLaunchPath(platformInfo); + // If the user has not provided a path, we'll use the locally-installed OmniSharp + const basePath = path.resolve(util.getExtensionPath(), '.omnisharp'); if (platformInfo.isWindows()) { - return launchWindows(launchPath, cwd, args); - } - else { - return launchNix(launchPath, cwd, args); + return launchWindows(path.join(basePath, 'OmniSharp.exe'), cwd, args); } + + // If it's possible to launch on a global Mono, we'll do that. Otherwise, run with our + // locally installed Mono runtime. + return canLaunchMono() + .then(() => { + return launchNixMono(path.join(basePath, 'omnisharp', 'OmniSharp.exe'), cwd, args); + }) + .catch(_ => { + return launchNix(path.join(basePath, 'run'), cwd, args); + }); }); } @@ -218,14 +282,6 @@ function getConfigurationValue(globalConfig: vscode.WorkspaceConfiguration, csha return globalConfig.get(configurationPath, defaultValue); } -function getLaunchPath(platformInfo: PlatformInformation): string { - const binPath = path.resolve(util.getExtensionPath(), '.omnisharp'); - - return platformInfo.isWindows() - ? path.join(binPath, 'OmniSharp.exe') - : path.join(binPath, 'run'); -} - function launchWindows(launchPath: string, cwd: string, args: string[]): LaunchResult { function escapeIfNeeded(arg: string) { const hasSpaceWithoutQuotes = /^[^"].* .*[^"]/; @@ -272,8 +328,8 @@ function launchNixMono(launchPath: string, cwd: string, args: string[]): Promise return canLaunchMono() .then(() => { let argsCopy = args.slice(0); // create copy of details args - argsCopy.unshift("--assembly-loader=strict"); argsCopy.unshift(launchPath); + argsCopy.unshift("--assembly-loader=strict"); let process = spawn('mono', argsCopy, { detached: false, diff --git a/src/omnisharp/options.ts b/src/omnisharp/options.ts index 5be1291373..96aa6219de 100644 --- a/src/omnisharp/options.ts +++ b/src/omnisharp/options.ts @@ -14,7 +14,11 @@ export class Options { public autoStart?: boolean, public projectLoadTimeout?: number, public maxProjectResults?: number, - public useEditorFormattingSettings?: boolean) { } + public useEditorFormattingSettings?: boolean, + public useFormatting?: boolean, + public showReferencesCodeLens?: boolean, + public showTestsCodeLens?: boolean, + public disableCodeActions?: boolean) { } public static Read(): Options { // Extra effort is taken below to ensure that legacy versions of options @@ -48,6 +52,24 @@ export class Options { const maxProjectResults = omnisharpConfig.get('maxProjectResults', 250); const useEditorFormattingSettings = omnisharpConfig.get('useEditorFormattingSettings', true); - return new Options(path, useMono, waitForDebugger, loggingLevel, autoStart, projectLoadTimeout, maxProjectResults, useEditorFormattingSettings); + const useFormatting = csharpConfig.get('format.enable', true); + + const showReferencesCodeLens = csharpConfig.get('referencesCodeLens.enabled', true); + const showTestsCodeLens = csharpConfig.get('testsCodeLens.enabled', true); + + const disableCodeActions = csharpConfig.get('disableCodeActions', false); + + return new Options(path, + useMono, + waitForDebugger, + loggingLevel, + autoStart, + projectLoadTimeout, + maxProjectResults, + useEditorFormattingSettings, + useFormatting, + showReferencesCodeLens, + showTestsCodeLens, + disableCodeActions); } } \ No newline at end of file diff --git a/src/omnisharp/protocol.ts b/src/omnisharp/protocol.ts index 422e1c9267..0532112bc7 100644 --- a/src/omnisharp/protocol.ts +++ b/src/omnisharp/protocol.ts @@ -256,6 +256,7 @@ export interface AutoCompleteRequest extends Request { WantSnippet?: boolean; WantReturnType?: boolean; WantKind?: boolean; + TriggerCharacter?: string; } export interface AutoCompleteResponse { @@ -279,6 +280,7 @@ export interface WorkspaceInformationResponse { MsBuild?: MsBuildWorkspaceInformation; DotNet?: DotNetWorkspaceInformation; ScriptCs?: ScriptCsContext; + Cake?: CakeContext; } export interface MsBuildWorkspaceInformation { @@ -294,6 +296,10 @@ export interface ScriptCsContext { Path: string; } +export interface CakeContext { + Path: string; +} + export interface MSBuildProject { ProjectGuid: string; Path: string; @@ -412,6 +418,17 @@ export interface PackageDependency { Name: string; Version: string; } + +export interface FilesChangedRequest extends Request{ + ChangeType: FileChangeType; +} + +export enum FileChangeType +{ + Change = "Change", + Create = "Create", + Delete = "Delete" +} export namespace V2 { @@ -501,6 +518,7 @@ export namespace V2 { export interface DebugTestGetStartInfoRequest extends Request { MethodName: string; TestFrameworkName: string; + TargetFrameworkVersion: string; } export interface DebugTestGetStartInfoResponse { @@ -526,6 +544,7 @@ export namespace V2 { export interface GetTestStartInfoRequest extends Request { MethodName: string; TestFrameworkName: string; + TargetFrameworkVersion: string; } export interface GetTestStartInfoResponse { @@ -537,6 +556,7 @@ export namespace V2 { export interface RunTestRequest extends Request { MethodName: string; TestFrameworkName: string; + TargetFrameworkVersion: string; } export module TestOutcomes { @@ -617,4 +637,44 @@ export function getDotNetCoreProjectDescriptors(info: WorkspaceInformationRespon } return result; +} + +export function findExecutableMSBuildProjects(projects: MSBuildProject[]) { + let result: MSBuildProject[] = []; + + projects.forEach(project => { + if (project.IsExe && findNetCoreAppTargetFramework(project) !== undefined) { + result.push(project); + } + }); + + return result; +} + +export function findExecutableProjectJsonProjects(projects: DotNetProject[], configurationName: string) { + let result: DotNetProject[] = []; + + projects.forEach(project => { + project.Configurations.forEach(configuration => { + if (configuration.Name === configurationName && configuration.EmitEntryPoint === true) { + if (project.Frameworks.length > 0) { + result.push(project); + } + } + }); + }); + + return result; +} + +export function containsDotNetCoreProjects(workspaceInfo: WorkspaceInformationResponse) { + if (workspaceInfo.DotNet && findExecutableProjectJsonProjects(workspaceInfo.DotNet.Projects, 'Debug').length > 0) { + return true; + } + + if (workspaceInfo.MsBuild && findExecutableMSBuildProjects(workspaceInfo.MsBuild.Projects).length > 0) { + return true; + } + + return false; } \ No newline at end of file diff --git a/src/omnisharp/server.ts b/src/omnisharp/server.ts index 98c4447379..b2dc6086e8 100644 --- a/src/omnisharp/server.ts +++ b/src/omnisharp/server.ts @@ -78,8 +78,6 @@ export class OmniSharpServer { private _channel: vscode.OutputChannel; private _logger: Logger; - private _isDebugEnable: boolean = false; - private _serverProcess: ChildProcess; private _options: Options; @@ -146,10 +144,6 @@ export class OmniSharpServer { return this._channel; } - public isDebugEnable(): boolean { - return this._isDebugEnable; - } - // --- eventing public onStdout(listener: (e: string) => any, thisArg?: any) { @@ -285,13 +279,6 @@ export class OmniSharpServer { this._fireEvent(Events.ServerStart, solutionPath); return this._doConnect(); - }).then(() => { - return vscode.commands.getCommands() - .then(commands => { - if (commands.find(c => c === 'vscode.startDebug')) { - this._isDebugEnable = true; - } - }); }).then(() => { // Start telemetry reporting this._telemetryIntervalId = setInterval(() => this._reportTelemetry(), TelemetryReportingDelay); @@ -367,11 +354,11 @@ export class OmniSharpServer { public autoStart(preferredPath: string): Thenable { return findLaunchTargets().then(launchTargets => { // If there aren't any potential launch targets, we create file watcher and try to - // start the server again once a *.sln, *.csproj, project.json or CSX file is created. + // start the server again once a *.sln, *.csproj, project.json, CSX or Cake file is created. if (launchTargets.length === 0) { return new Promise((resolve, reject) => { // 1st watch for files - let watcher = vscode.workspace.createFileSystemWatcher('{**/*.sln,**/*.csproj,**/project.json,**/*.csx}', + let watcher = vscode.workspace.createFileSystemWatcher('{**/*.sln,**/*.csproj,**/project.json,**/*.csx,**/*.cake}', /*ignoreCreateEvents*/ false, /*ignoreChangeEvents*/ true, /*ignoreDeleteEvents*/ true); @@ -496,6 +483,8 @@ export class OmniSharpServer { } private _onLineReceived(line: string) { + line = line.trim(); + if (line[0] !== '{') { this._logger.appendLine(line); return; diff --git a/src/omnisharp/typeConvertion.ts b/src/omnisharp/typeConvertion.ts index 161df401d8..532d026a00 100644 --- a/src/omnisharp/typeConvertion.ts +++ b/src/omnisharp/typeConvertion.ts @@ -9,6 +9,10 @@ import * as vscode from 'vscode'; export function toLocation(location: protocol.ResourceLocation | protocol.QuickFix): vscode.Location { const fileName = vscode.Uri.file(location.FileName); + return toLocationFromUri(fileName, location); +} + +export function toLocationFromUri(uri: vscode.Uri, location: protocol.ResourceLocation | protocol.QuickFix): vscode.Location { const position = new vscode.Position(location.Line - 1, location.Column - 1); const endLine = (location).EndLine; @@ -16,10 +20,10 @@ export function toLocation(location: protocol.ResourceLocation | protocol.QuickF if (endLine !== undefined && endColumn !== undefined) { const endPosition = new vscode.Position(endLine - 1, endColumn - 1); - return new vscode.Location(fileName, new vscode.Range(position, endPosition)); + return new vscode.Location(uri, new vscode.Range(position, endPosition)); } - return new vscode.Location(fileName, position); + return new vscode.Location(uri, position); } export function toRange(rangeLike: { Line: number; Column: number; EndLine: number; EndColumn: number; }): vscode.Range { diff --git a/src/platform.ts b/src/platform.ts index e33b8758c4..eab4935be5 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as crypto from 'crypto'; import * as fs from 'fs'; import * as os from 'os'; import * as util from './common'; @@ -33,6 +34,34 @@ export class LinuxDistribution { return `name=${this.name}, version=${this.version}`; } + /** + * Returns a string representation of LinuxDistribution that only returns the + * distro name if it appears on an allowed list of known distros. Otherwise, + * it returns 'other'. + */ + public toTelemetryString(): string { + const allowedList = [ + 'antergos', 'arch', 'centos', 'debian', 'deepin', 'elementary', 'fedora', + 'galliumos', 'gentoo', 'kali', 'linuxmint', 'manjoro', 'neon', 'opensuse', + 'parrot', 'rhel', 'ubuntu', 'zorin' + ]; + + if (this.name === unknown || allowedList.indexOf(this.name) >= 0) { + return this.toString(); + } + else { + // Having a hash of the name will be helpful to identify spikes in the 'other' + // bucket when a new distro becomes popular and needs to be added to the + // allowed list above. + const hash = crypto.createHash('sha256'); + hash.update(this.name); + + const hashedName = hash.digest('hex'); + + return `other (${hashedName})`; + } + } + private static FromFilePath(filePath: string): Promise { return new Promise((resolve, reject) => { fs.readFile(filePath, 'utf8', (error, data) => { diff --git a/src/tools/OptionsSchema.json b/src/tools/OptionsSchema.json index 96b1cde993..cb8c9fb69d 100644 --- a/src/tools/OptionsSchema.json +++ b/src/tools/OptionsSchema.json @@ -9,7 +9,7 @@ "type": "object", "description": "Platform-specific pipe launch configuration options", "default": { - "pipeCwd": "${workspaceRoot}", + "pipeCwd": "${workspaceFolder}", "pipeProgram": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'", "pipeArgs": [] }, @@ -17,7 +17,7 @@ "pipeCwd": { "type": "string", "description": "The fully qualified path to the working directory for the pipe program.", - "default": "${workspaceRoot}" + "default": "${workspaceFolder}" }, "pipeProgram": { "type": "string", @@ -61,7 +61,7 @@ "required": ["debuggerPath"], "description": "When present, this tells the debugger to connect to a remote computer using another executable as a pipe that will relay standard input/output between VS Code and the .NET Core debugger backend executable (vsdbg).", "default": { - "pipeCwd": "${workspaceRoot}", + "pipeCwd": "${workspaceFolder}", "pipeProgram": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'", "pipeArgs": [], "debuggerPath" : "enter the path for the debugger on the target machine, for example ~/vsdbg/vsdbg" @@ -70,7 +70,7 @@ "pipeCwd": { "type": "string", "description": "The fully qualified path to the working directory for the pipe program.", - "default": "${workspaceRoot}" + "default": "${workspaceFolder}" }, "pipeProgram": { "type": "string", @@ -116,7 +116,7 @@ "$ref": "#/definitions/PipeConfigurations", "description": "Windows-specific pipe launch configuration options", "default": { - "pipeCwd": "${workspaceRoot}", + "pipeCwd": "${workspaceFolder}", "pipeProgram": "enter the fully qualified path for the pipe program name, for example 'c:\\tools\\plink.exe'", "pipeArgs": [] } @@ -125,7 +125,7 @@ "$ref": "#/definitions/PipeConfigurations", "description": "OSX-specific pipe launch configuration options", "default": { - "pipeCwd": "${workspaceRoot}", + "pipeCwd": "${workspaceFolder}", "pipeProgram": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'", "pipeArgs": [] } @@ -134,7 +134,7 @@ "$ref": "#/definitions/PipeConfigurations", "description": "Linux-specific pipe launch configuration options", "default": { - "pipeCwd": "${workspaceRoot}", + "pipeCwd": "${workspaceFolder}", "pipeProgram": "enter the fully qualified path for the pipe program name, for example '/usr/bin/ssh'", "pipeArgs": [] } @@ -261,13 +261,13 @@ "properties": { "program": { "type": "string", - "description": "Path to the application dll or .NET Core host executable to launch.\nThis property normally takes the form: '${workspaceRoot}/bin/Debug/(target-framework)/(project-name.dll)'\nExample: '${workspaceRoot}/bin/Debug/netcoreapp1.1/MyProject.dll'\n\nWhere:\n(target-framework) is the framework that the debugged project is being built for. This is normally found in the project file as the 'TargetFramework' property.\n(project-name.dll) is the name of debugged project's build output dll. This is normally the same as the project file name but with a '.dll' extension.", - "default": "${workspaceRoot}/bin/Debug//.dll" + "description": "Path to the application dll or .NET Core host executable to launch.\nThis property normally takes the form: '${workspaceFolder}/bin/Debug/(target-framework)/(project-name.dll)'\nExample: '${workspaceFolder}/bin/Debug/netcoreapp1.1/MyProject.dll'\n\nWhere:\n(target-framework) is the framework that the debugged project is being built for. This is normally found in the project file as the 'TargetFramework' property.\n(project-name.dll) is the name of debugged project's build output dll. This is normally the same as the project file name but with a '.dll' extension.", + "default": "${workspaceFolder}/bin/Debug//.dll" }, "cwd": { "type": "string", "description": "Path to the working directory of the program being debugged. Default is the current workspace.", - "default": "${workspaceRoot}" + "default": "${workspaceFolder}" }, "args": { "anyOf": [ diff --git a/test/assets.test.ts b/test/assets.test.ts index 415bec81dd..fd3effd2ae 100644 --- a/test/assets.test.ts +++ b/test/assets.test.ts @@ -17,11 +17,11 @@ suite("Asset generation: project.json", () => { let info = createDotNetWorkspaceInformation(rootPath, 'testApp.dll', 'netcoreapp1.0'); let generator = new AssetGenerator(info, rootPath); let tasksJson = generator.createTasksConfiguration(); - let buildPath = tasksJson.tasks[0].args[0]; + let buildPath = tasksJson.tasks[0].args[1]; - // ${workspaceRoot}/project.json + // ${workspaceFolder}/project.json let segments = buildPath.split(path.posix.sep); - segments.should.deep.equal(['${workspaceRoot}', 'project.json']); + segments.should.deep.equal(['${workspaceFolder}', 'project.json']); }); test("Create tasks.json for nested project opened in workspace", () => { @@ -29,11 +29,11 @@ suite("Asset generation: project.json", () => { let info = createDotNetWorkspaceInformation(path.join(rootPath, 'nested'), 'testApp.dll', 'netcoreapp1.0'); let generator = new AssetGenerator(info, rootPath); let tasksJson = generator.createTasksConfiguration(); - let buildPath = tasksJson.tasks[0].args[0]; + let buildPath = tasksJson.tasks[0].args[1]; - // ${workspaceRoot}/nested/project.json + // ${workspaceFolder}/nested/project.json let segments = buildPath.split(path.posix.sep); - segments.should.deep.equal(['${workspaceRoot}', 'nested', 'project.json']); + segments.should.deep.equal(['${workspaceFolder}', 'nested', 'project.json']); }); test("Create launch.json for project opened in workspace", () => { @@ -43,9 +43,9 @@ suite("Asset generation: project.json", () => { let launchJson = parse(generator.createLaunchJson(/*isWebProject*/ false), undefined, { disallowComments: true }); let programPath = launchJson[0].program; - // ${workspaceRoot}/bin/Debug/netcoreapp1.0/testApp.dll + // ${workspaceFolder}/bin/Debug/netcoreapp1.0/testApp.dll let segments = programPath.split(path.posix.sep); - segments.should.deep.equal(['${workspaceRoot}', 'bin', 'Debug', 'netcoreapp1.0', 'testApp.dll']); + segments.should.deep.equal(['${workspaceFolder}', 'bin', 'Debug', 'netcoreapp1.0', 'testApp.dll']); }); test("Create launch.json for nested project opened in workspace", () => { @@ -55,9 +55,9 @@ suite("Asset generation: project.json", () => { let launchJson = parse(generator.createLaunchJson(/*isWebProject*/ false), undefined, { disallowComments: true }); let programPath = launchJson[0].program; - // ${workspaceRoot}/nested/bin/Debug/netcoreapp1.0/testApp.dll + // ${workspaceFolder}/nested/bin/Debug/netcoreapp1.0/testApp.dll let segments = programPath.split(path.posix.sep); - segments.should.deep.equal(['${workspaceRoot}', 'nested', 'bin', 'Debug', 'netcoreapp1.0', 'testApp.dll']); + segments.should.deep.equal(['${workspaceFolder}', 'nested', 'bin', 'Debug', 'netcoreapp1.0', 'testApp.dll']); }); test("Create launch.json for web project opened in workspace", () => { @@ -67,9 +67,9 @@ suite("Asset generation: project.json", () => { let launchJson = parse(generator.createLaunchJson(/*isWebProject*/ true), undefined, { disallowComments: true }); let programPath = launchJson[0].program; - // ${workspaceRoot}/bin/Debug/netcoreapp1.0/testApp.dll + // ${workspaceFolder}/bin/Debug/netcoreapp1.0/testApp.dll let segments = programPath.split(path.posix.sep); - segments.should.deep.equal(['${workspaceRoot}', 'bin', 'Debug', 'netcoreapp1.0', 'testApp.dll']); + segments.should.deep.equal(['${workspaceFolder}', 'bin', 'Debug', 'netcoreapp1.0', 'testApp.dll']); }); test("Create launch.json for nested web project opened in workspace", () => { @@ -79,9 +79,9 @@ suite("Asset generation: project.json", () => { let launchJson = parse(generator.createLaunchJson(/*isWebProject*/ true), undefined, { disallowComments: true }); let programPath = launchJson[0].program; - // ${workspaceRoot}/nested/bin/Debug/netcoreapp1.0/testApp.dll + // ${workspaceFolder}/nested/bin/Debug/netcoreapp1.0/testApp.dll let segments = programPath.split(path.posix.sep); - segments.should.deep.equal(['${workspaceRoot}', 'nested', 'bin', 'Debug', 'netcoreapp1.0', 'testApp.dll']); + segments.should.deep.equal(['${workspaceFolder}', 'nested', 'bin', 'Debug', 'netcoreapp1.0', 'testApp.dll']); }); }); @@ -125,11 +125,11 @@ suite("Asset generation: csproj", () => { let info = createMSBuildWorkspaceInformation(path.join(rootPath, 'testApp.csproj'), 'testApp', 'netcoreapp1.0'); let generator = new AssetGenerator(info, rootPath); let tasksJson = generator.createTasksConfiguration(); - let buildPath = tasksJson.tasks[0].args[0]; + let buildPath = tasksJson.tasks[0].args[1]; - // ${workspaceRoot}/project.json + // ${workspaceFolder}/project.json let segments = buildPath.split(path.posix.sep); - segments.should.deep.equal(['${workspaceRoot}', 'testApp.csproj']); + segments.should.deep.equal(['${workspaceFolder}', 'testApp.csproj']); }); test("Create tasks.json for nested project opened in workspace", () => { @@ -137,11 +137,11 @@ suite("Asset generation: csproj", () => { let info = createMSBuildWorkspaceInformation(path.join(rootPath, 'nested', 'testApp.csproj'), 'testApp', 'netcoreapp1.0'); let generator = new AssetGenerator(info, rootPath); let tasksJson = generator.createTasksConfiguration(); - let buildPath = tasksJson.tasks[0].args[0]; + let buildPath = tasksJson.tasks[0].args[1]; - // ${workspaceRoot}/nested/project.json + // ${workspaceFolder}/nested/project.json let segments = buildPath.split(path.posix.sep); - segments.should.deep.equal(['${workspaceRoot}', 'nested', 'testApp.csproj']); + segments.should.deep.equal(['${workspaceFolder}', 'nested', 'testApp.csproj']); }); test("Create launch.json for project opened in workspace", () => { @@ -151,9 +151,9 @@ suite("Asset generation: csproj", () => { let launchJson = parse(generator.createLaunchJson(/*isWebProject*/ false), undefined, { disallowComments: true }); let programPath = launchJson[0].program; - // ${workspaceRoot}/bin/Debug/netcoreapp1.0/testApp.dll + // ${workspaceFolder}/bin/Debug/netcoreapp1.0/testApp.dll let segments = programPath.split(path.posix.sep); - segments.should.deep.equal(['${workspaceRoot}', 'bin', 'Debug', 'netcoreapp1.0', 'testApp.dll']); + segments.should.deep.equal(['${workspaceFolder}', 'bin', 'Debug', 'netcoreapp1.0', 'testApp.dll']); }); test("Create launch.json for nested project opened in workspace", () => { @@ -163,9 +163,9 @@ suite("Asset generation: csproj", () => { let launchJson = parse(generator.createLaunchJson(/*isWebProject*/ false), undefined, { disallowComments: true }); let programPath = launchJson[0].program; - // ${workspaceRoot}/nested/bin/Debug/netcoreapp1.0/testApp.dll + // ${workspaceFolder}/nested/bin/Debug/netcoreapp1.0/testApp.dll let segments = programPath.split(path.posix.sep); - segments.should.deep.equal(['${workspaceRoot}', 'nested', 'bin', 'Debug', 'netcoreapp1.0', 'testApp.dll']); + segments.should.deep.equal(['${workspaceFolder}', 'nested', 'bin', 'Debug', 'netcoreapp1.0', 'testApp.dll']); }); test("Create launch.json for web project opened in workspace", () => { @@ -175,9 +175,9 @@ suite("Asset generation: csproj", () => { let launchJson = parse(generator.createLaunchJson(/*isWebProject*/ true), undefined, { disallowComments: true }); let programPath = launchJson[0].program; - // ${workspaceRoot}/bin/Debug/netcoreapp1.0/testApp.dll + // ${workspaceFolder}/bin/Debug/netcoreapp1.0/testApp.dll let segments = programPath.split(path.posix.sep); - segments.should.deep.equal(['${workspaceRoot}', 'bin', 'Debug', 'netcoreapp1.0', 'testApp.dll']); + segments.should.deep.equal(['${workspaceFolder}', 'bin', 'Debug', 'netcoreapp1.0', 'testApp.dll']); }); test("Create launch.json for nested web project opened in workspace", () => { @@ -187,9 +187,9 @@ suite("Asset generation: csproj", () => { let launchJson = parse(generator.createLaunchJson(/*isWebProject*/ true), undefined, { disallowComments: true }); let programPath = launchJson[0].program; - // ${workspaceRoot}/nested/bin/Debug/netcoreapp1.0/testApp.dll + // ${workspaceFolder}/nested/bin/Debug/netcoreapp1.0/testApp.dll let segments = programPath.split(path.posix.sep); - segments.should.deep.equal(['${workspaceRoot}', 'nested', 'bin', 'Debug', 'netcoreapp1.0', 'testApp.dll']); + segments.should.deep.equal(['${workspaceFolder}', 'nested', 'bin', 'Debug', 'netcoreapp1.0', 'testApp.dll']); }); }); diff --git a/test/common.test.ts b/test/common.test.ts index cf90b15fe0..483586fc84 100644 --- a/test/common.test.ts +++ b/test/common.test.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as path from 'path'; import { should } from 'chai'; -import { buildPromiseChain, safeLength, sum } from '../src/common'; +import { buildPromiseChain, safeLength, sum, isSubfolderOf } from '../src/common'; suite("Common", () => { suiteSetup(() => should()); @@ -64,4 +65,30 @@ suite("Common", () => { result.should.equal(5); }); }); + + suite("isSubfolderOf", () => { + test("same paths", () => { + let subfolder: string = ["C:", "temp", "VS", "dotnetProject"].join(path.sep); + let folder: string= ["C:", "temp", "VS", "dotnetProject"].join(path.sep); + isSubfolderOf(subfolder, folder).should.be.true; + }); + + test("correct subfolder", () => { + let subfolder: string = ["C:", "temp", "VS"].join(path.sep); + let folder: string= ["C:", "temp", "VS", "dotnetProject"].join(path.sep); + isSubfolderOf(subfolder, folder).should.be.true; + }); + + test("longer subfolder", () => { + let subfolder: string = ["C:", "temp", "VS", "a", "b", "c"].join(path.sep); + let folder: string= ["C:", "temp", "VS"].join(path.sep); + isSubfolderOf(subfolder, folder).should.be.false; + }); + + test("Different drive", () => { + let subfolder: string = ["C:", "temp", "VS"].join(path.sep); + let folder: string= ["E:", "temp", "VS"].join(path.sep); + isSubfolderOf(subfolder, folder).should.be.false; + }); + }); }); diff --git a/test/processPicker.test.ts b/test/processPicker.test.ts index 63ad82f8f5..cfbcaf5d33 100644 --- a/test/processPicker.test.ts +++ b/test/processPicker.test.ts @@ -156,7 +156,7 @@ suite("Remote Process Picker: Validate quoting arguments.", () => { function GetWindowsWSLLaunchJSONWithArrayArgs() { return { - pipeCwd: "${workspaceRoot}", + pipeCwd: "${workspaceFolder}", pipeProgram: "C:\\System32\\bash.exe", pipeArgs: ["-c"] } @@ -164,7 +164,7 @@ function GetWindowsWSLLaunchJSONWithArrayArgs() { function GetWindowsWSLLaunchJSONWithArrayArgsAndDebuggerCommand() { return { - pipeCwd: "${workspaceRoot}", + pipeCwd: "${workspaceFolder}", pipeProgram: "C:\\System32\\bash.exe", pipeArgs: ["-c", "${debuggerCommand}", "--", "ignored"] } @@ -172,7 +172,7 @@ function GetWindowsWSLLaunchJSONWithArrayArgsAndDebuggerCommand() { function GetWindowsWSLLaunchJSONWithStringArgs() { return { - pipeCwd: "${workspaceRoot}", + pipeCwd: "${workspaceFolder}", pipeProgram: "C:\\System32\\bash.exe", pipeArgs: "-c" } @@ -180,7 +180,7 @@ function GetWindowsWSLLaunchJSONWithStringArgs() { function GetWindowsWSLLaunchJSONWithStringArgsAndDebuggerCommand() { return { - pipeCwd: "${workspaceRoot}", + pipeCwd: "${workspaceFolder}", pipeProgram: "C:\\System32\\bash.exe", pipeArgs: "-c ${debuggerCommand} -- ignored" } @@ -188,7 +188,7 @@ function GetWindowsWSLLaunchJSONWithStringArgsAndDebuggerCommand() { function GetWindowsDockerLaunchJSONWithArrayArgs() { return { - pipeCwd: "${workspaceRoot}", + pipeCwd: "${workspaceFolder}", pipeProgram: "docker", pipeArgs: ["-i", "exec", "1234567"], quoteArgs: false @@ -197,7 +197,7 @@ function GetWindowsDockerLaunchJSONWithArrayArgs() { function GetWindowsDockerLaunchJSONWithStringArgsAndDebuggerCommand() { return { - pipeCwd: "${workspaceRoot}", + pipeCwd: "${workspaceFolder}", pipeProgram: "docker", pipeArgs: "-i exec 1234567 ${debuggerCommand}", quoteArgs: false @@ -206,7 +206,7 @@ function GetWindowsDockerLaunchJSONWithStringArgsAndDebuggerCommand() { function GetLinuxLaunchJSONWithArrayArgs() { return { - pipeCwd: "${workspaceRoot}", + pipeCwd: "${workspaceFolder}", pipeProgram: "/usr/bin/shared/dotnet", pipeArgs: ["bin/framework/myprogram.dll", "argument with spaces"], quoteArg: true @@ -215,7 +215,7 @@ function GetLinuxLaunchJSONWithArrayArgs() { function GetOSSpecificJSON() { return { - pipeCwd: "${workspaceRoot}", + pipeCwd: "${workspaceFolder}", pipeProgram: "pipeProgram", pipeArgs: [], windows: { diff --git a/typings/vscode-tasks.d.ts b/typings/vscode-tasks.d.ts index a4a4245f7d..aea710e271 100644 --- a/typings/vscode-tasks.d.ts +++ b/typings/vscode-tasks.d.ts @@ -32,25 +32,21 @@ declare module "vscode-tasks" { export interface BaseTaskConfiguration { /** - * The command to be executed. Can be an external program or a shell - * command. + * The type of a custom task. Tasks of type "shell" are executed + * inside a shell (e.g. bash, cmd, powershell, ...) */ - command: string; + type?: "shell" | "process"; /** - * Specifies whether the command is a shell command and therefore must - * be executed in a shell interpreter (e.g. cmd.exe, bash, ...). - * - * Defaults to false if omitted. + * The command to be executed. Can be an external program or a shell + * command. */ - isShellCommand?: boolean; + command?: string; /** - * Specifies whether a global command is watching the filesystem. A task.json - * file can either contain a global isWatching property or a tasks property - * but not both. + * Specifies whether a global command is a background task. */ - isWatching?: boolean; + isBackground?: boolean; /** * The command options used when the command is executed. Can be omitted. @@ -63,30 +59,9 @@ declare module "vscode-tasks" { args?: string[]; /** - * Controls whether the output view of the running tasks is brought to front or not. - * - * Valid values are: - * "always": bring the output window always to front when a task is executed. - * "silent": only bring it to front if no problem matcher is defined for the task executed. - * "never": never bring the output window to front. - * - * If omitted "always" is used. + * The presentation options. */ - showOutput?: string; - - /** - * If set to false the task name is added as an additional argument to the - * command when executed. If set to true the task name is suppressed. If - * omitted false is used. - */ - suppressTaskName?: boolean; - - /** - * Some commands require that the task argument is highlighted with a special - * prefix (e.g. /t: for msbuild). This property can be used to control such - * a prefix. - */ - taskSelector?: string; + presentation?: PresentationOptions; /** * The problem matcher to be used if a global command is executed (e.g. no tasks @@ -102,6 +77,7 @@ declare module "vscode-tasks" { tasks?: TaskDescription[]; } + /** * Options to be passed to the external program or shell */ @@ -118,6 +94,23 @@ declare module "vscode-tasks" { * the parent process' environment is used. */ env?: { [key: string]: string; }; + + /** + * Configuration of the shell when task type is `shell` + */ + shell: { + + /** + * The shell to use. + */ + executable: string; + + /** + * The arguments to be passed to the shell executable to run in command mode + * (e.g ['-c'] for bash or ['/S', '/C'] for cmd.exe). + */ + args?: string[]; + } } /** @@ -131,36 +124,42 @@ declare module "vscode-tasks" { taskName: string; /** - * Additional arguments passed to the command when this task is - * executed. + * The type of a custom task. Tasks of type "shell" are executed + * inside a shell (e.g. bash, cmd, powershell, ...) */ - args?: string[]; + type: "shell" | "process"; /** - * Whether this task maps to the default build command. + * The command to execute. If the type is "shell" it should be the full + * command line including any additional arguments passed to the command. */ - isBuildCommand?: boolean; + command: string; /** - * Whether this task maps to the default test command. + * Whether the executed command is kept alive and runs in the background. */ - isTestCommand?: boolean; + isBackground?: boolean; /** - * Whether the executed command is kept alive and is watching the file system. + * Additional arguments passed to the command. Should be used if type + * is "process". */ - isWatching?: boolean; + args?: string[]; /** - * Controls whether the output view of the running tasks is brought to front or not. - * See BaseTaskConfiguration#showOutput for details. + * Tasks V1 isBuildCommand is used to detect if the tasks is in a build group. */ - showOutput?: string; + isBuildCommand?: boolean; /** - * See BaseTaskConfiguration#suppressTaskName for details. + * Defines the group to which this tasks belongs */ - suppressTaskName?: boolean; + group?: "build" | "string"; + + /** + * The presentation options. + */ + presentation?: PresentationOptions; /** * The problem matcher(s) to use to capture problems in the tasks @@ -169,6 +168,33 @@ declare module "vscode-tasks" { problemMatcher?: string | ProblemMatcher | (string | ProblemMatcher)[]; } + export interface PresentationOptions { + + /** + * Controls whether the task output is reveal in the user interface. + * Defaults to `always`. + */ + reveal?: "never" | "silent" | "always"; + + /** + * Controls whether the command associated with the task is echoed + * in the user interface. + */ + echo?: boolean; + + /** + * Controls whether the panel showing the task output is taking focus. + */ + focus?: boolean; + + /** + * Controls if the task panel is used for this task only (dedicated), + * shared between tasks (shared) or if a new panel is created on + * every task execution (new). Defaults to `shared` + */ + panel?: "shared" | "dedicated" | "new"; + } + /** * A description of a problem matcher that detects problems * in build output. @@ -216,7 +242,7 @@ declare module "vscode-tasks" { fileLocation?: string | string[]; /** - * The name of a predefined problem pattern, the inline definintion + * The name of a predefined problem pattern, the inline definition * of a problem pattern or an array of problem patterns to match * problems spread over multiple lines. */ @@ -226,13 +252,13 @@ declare module "vscode-tasks" { * Additional information used to detect when a background task (like a watching task in Gulp) * is active. */ - watching?: WatchingMatcher; + background?: BackgroundMatcher; } /** - * A description to track the start and end of a watching task. + * A description to track the start and end of a background task. */ - export interface WatchingMatcher { + export interface BackgroundMatcher { /** * If set to true the watcher is in active mode when the task @@ -242,12 +268,12 @@ declare module "vscode-tasks" { activeOnStart?: boolean; /** - * If matched in the output the start of a watching task is signaled. + * If matched in the output the start of a background task is signaled. */ beginsPattern?: string; /** - * If matched in the output the end of a watching task is signaled. + * If matched in the output the end of a background task is signaled. */ endsPattern?: string; } @@ -306,7 +332,7 @@ declare module "vscode-tasks" { severity?: number; /** - * The match group index of the problems's code. + * The match group index of the problem's code. * * Defaults to undefined. No code is captured. */