diff --git a/src/features/commands.ts b/src/features/commands.ts index a5b9a755a..8775eb856 100644 --- a/src/features/commands.ts +++ b/src/features/commands.ts @@ -5,7 +5,7 @@ import { OmniSharpServer } from '../omnisharp/server'; import * as serverUtils from '../omnisharp/utils'; -import { findLaunchTargets } from '../omnisharp/launcher'; +import { findLaunchTargets, LaunchTarget } from '../omnisharp/launcher'; import * as cp from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; @@ -92,14 +92,18 @@ async function pickProjectAndStart(server: OmniSharpServer, optionProvider: Opti } } - return vscode.window.showQuickPick(targets, { - matchOnDescription: true, - placeHolder: `Select 1 of ${targets.length} projects` - }).then(async launchTarget => { - if (launchTarget) { - return server.restart(launchTarget); - } - }); + return showProjectSelector(server, targets); + }); +} + +export async function showProjectSelector(server: OmniSharpServer, targets: LaunchTarget[]): Promise { + return vscode.window.showQuickPick(targets, { + matchOnDescription: true, + placeHolder: `Select 1 of ${targets.length} projects` + }).then(async launchTarget => { + if (launchTarget) { + return server.restart(launchTarget); + } }); } diff --git a/src/observers/ProjectStatusBarObserver.ts b/src/observers/ProjectStatusBarObserver.ts index 61c620841..8e2c4fd74 100644 --- a/src/observers/ProjectStatusBarObserver.ts +++ b/src/observers/ProjectStatusBarObserver.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { basename } from 'path'; +import { basename, join } from 'path'; import { BaseEvent, WorkspaceInformationUpdated } from "../omnisharp/loggingEvents"; import { BaseStatusBarItemObserver } from './BaseStatusBarItemObserver'; import { EventType } from '../omnisharp/EventType'; @@ -25,9 +25,24 @@ export class ProjectStatusBarObserver extends BaseStatusBarItemObserver { private handleWorkspaceInformationUpdated(event: WorkspaceInformationUpdated) { let label: string; - let info = event.info; - if (info.MsBuild && info.MsBuild.SolutionPath) { - label = basename(info.MsBuild.SolutionPath); //workspace.getRelativePath(info.MsBuild.SolutionPath); + let msbuild = event.info.MsBuild; + if (msbuild && msbuild.SolutionPath) { + if (msbuild.SolutionPath.endsWith(".sln")) { + label = basename(msbuild.SolutionPath); + } + else { + // a project file was open, determine which project + for (const project of msbuild.Projects) { + // Get the project name. + label = basename(project.Path); + + // The solution path is the folder containing the open project. Combine it with the + // project name and see if it matches the project's path. + if (join(msbuild.SolutionPath, label) === project.Path) { + break; + } + } + } this.SetAndShowStatusBar('$(file-directory) ' + label, 'o.pickProjectAndStart'); } else { diff --git a/src/omnisharp/launcher.ts b/src/omnisharp/launcher.ts index 3752c5fe6..0f8a397b5 100644 --- a/src/omnisharp/launcher.ts +++ b/src/omnisharp/launcher.ts @@ -14,6 +14,7 @@ import { IMonoResolver } from '../constants/IMonoResolver'; export enum LaunchTargetKind { Solution, + Project, ProjectJson, Folder, Csx, @@ -115,82 +116,81 @@ export function resourcesToLaunchTargets(resources: vscode.Uri[]): LaunchTarget[ } } - let targets: LaunchTarget[] = []; + return resourcesAndFolderMapToLaunchTargets(resources, vscode.workspace.workspaceFolders.concat(), workspaceFolderToUriMap); +} - workspaceFolderToUriMap.forEach((resources, folderIndex) => { - let hasCsProjFiles = false, - hasSlnFile = false, - hasProjectJson = false, - hasProjectJsonAtRoot = false, - hasCSX = false, - hasCake = false, - hasCs = false; +export function resourcesAndFolderMapToLaunchTargets(resources: vscode.Uri[], workspaceFolders: vscode.WorkspaceFolder[], workspaceFolderToUriMap: Map): LaunchTarget[] { + let solutionTargets: LaunchTarget[] = []; + let projectJsonTargets: LaunchTarget[] = []; + let projectTargets: LaunchTarget[] = []; + let otherTargets: LaunchTarget[] = []; - hasCsProjFiles = resources.some(isCSharpProject); + workspaceFolderToUriMap.forEach((resources, folderIndex) => { + let hasProjectJsonAtRoot = false; + let hasCSX = false; + let hasCake = false; + let hasCs = false; - let folder = vscode.workspace.workspaceFolders[folderIndex]; + let folder = workspaceFolders[folderIndex]; let folderPath = folder.uri.fsPath; resources.forEach(resource => { - // Add .sln and .slnf files if there are .csproj files - if (hasCsProjFiles && isSolution(resource)) { - hasSlnFile = true; - targets.push({ + // Add .sln and .slnf files + if (isSolution(resource)) { + const dirname = path.dirname(resource.fsPath); + solutionTargets.push({ label: path.basename(resource.fsPath), - description: vscode.workspace.asRelativePath(path.dirname(resource.fsPath)), + description: vscode.workspace.asRelativePath(dirname), target: resource.fsPath, directory: path.dirname(resource.fsPath), kind: LaunchTargetKind.Solution }); } - // Add project.json files - if (isProjectJson(resource)) { + else if (isProjectJson(resource)) { const dirname = path.dirname(resource.fsPath); - hasProjectJson = true; hasProjectJsonAtRoot = hasProjectJsonAtRoot || dirname === folderPath; - - targets.push({ + projectJsonTargets.push({ label: path.basename(resource.fsPath), - description: vscode.workspace.asRelativePath(path.dirname(resource.fsPath)), + description: vscode.workspace.asRelativePath(dirname), target: dirname, directory: dirname, kind: LaunchTargetKind.ProjectJson }); } - - // Discover if there is any CSX file - if (!hasCSX && isCsx(resource)) { - hasCSX = true; + // Add .csproj files + else if (isCSharpProject(resource)) { + const dirname = path.dirname(resource.fsPath); + // OmniSharp doesn't support opening a project directly, however, it will open a project if + // we pass a folder path which contains a single .csproj. This is similar to how project.json + // is supported. + projectTargets.push({ + label: path.basename(resource.fsPath), + description: vscode.workspace.asRelativePath(dirname), + target: dirname, + directory: dirname, + kind: LaunchTargetKind.Project + }); } + else { + // Discover if there is any CSX file + hasCSX ||= isCsx(resource); - // Discover if there is any Cake file - if (!hasCake && isCake(resource)) { - hasCake = true; - } + // Discover if there is any Cake file + hasCake ||= isCake(resource); - //Discover if there is any cs file - if (!hasCs && isCs(resource)) { - hasCs = true; + //Discover if there is any cs file + hasCs ||= isCs(resource); } }); - // Add the root folder under the following circumstances: - // * If there are .csproj files, but no .sln or .slnf 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(folderPath), - description: '', - target: folderPath, - directory: folderPath, - kind: LaunchTargetKind.Folder - }); - } + const hasCsProjFiles = projectTargets.length > 0; + const hasSlnFile = solutionTargets.length > 0; + const hasProjectJson = projectJsonTargets.length > 0; // if we noticed any CSX file(s), add a single CSX-specific target pointing at the root folder if (hasCSX) { - targets.push({ + otherTargets.push({ label: "CSX", description: path.basename(folderPath), target: folderPath, @@ -201,7 +201,7 @@ export function resourcesToLaunchTargets(resources: vscode.Uri[]): LaunchTarget[ // if we noticed any Cake file(s), add a single Cake-specific target pointing at the root folder if (hasCake) { - targets.push({ + otherTargets.push({ label: "Cake", description: path.basename(folderPath), target: folderPath, @@ -211,7 +211,7 @@ export function resourcesToLaunchTargets(resources: vscode.Uri[]): LaunchTarget[ } if (hasCs && !hasSlnFile && !hasCsProjFiles && !hasProjectJson && !hasProjectJsonAtRoot) { - targets.push({ + otherTargets.push({ label: path.basename(folderPath), description: '', target: folderPath, @@ -221,7 +221,11 @@ export function resourcesToLaunchTargets(resources: vscode.Uri[]): LaunchTarget[ } }); - return targets.sort((a, b) => a.directory.localeCompare(b.directory)); + solutionTargets = solutionTargets.sort((a, b) => a.directory.localeCompare(b.directory)); + projectJsonTargets = projectJsonTargets.sort((a, b) => a.directory.localeCompare(b.directory)); + projectTargets = projectTargets.sort((a, b) => a.directory.localeCompare(b.directory)); + + return otherTargets.concat(solutionTargets).concat(projectJsonTargets).concat(projectTargets); } function isCSharpProject(resource: vscode.Uri): boolean { diff --git a/src/omnisharp/server.ts b/src/omnisharp/server.ts index 9945afc38..39fdb3efa 100644 --- a/src/omnisharp/server.ts +++ b/src/omnisharp/server.ts @@ -31,6 +31,7 @@ import CompositeDisposable from '../CompositeDisposable'; import Disposable from '../Disposable'; import OptionProvider from '../observers/OptionProvider'; import { IMonoResolver } from '../constants/IMonoResolver'; +import { showProjectSelector } from '../features/commands'; import { removeBOMFromBuffer, removeBOMFromString } from '../utils/removeBOM'; enum ServerState { @@ -537,10 +538,13 @@ export class OmniSharpServer { return this.autoStart(preferredPath); }); } - - const defaultLaunchSolutionConfigValue = this.optionProvider.GetLatestOptions().defaultLaunchSolution; + else if (launchTargets.length === 1) { + // If there's only one target, just start + return this.restart(launchTargets[0]); + } // First, try to launch against something that matches the user's preferred target + const defaultLaunchSolutionConfigValue = this.optionProvider.GetLatestOptions().defaultLaunchSolution; const defaultLaunchSolutionTarget = launchTargets.find((a) => (path.basename(a.target) === defaultLaunchSolutionConfigValue)); if (defaultLaunchSolutionTarget) { return this.restart(defaultLaunchSolutionTarget); @@ -549,21 +553,15 @@ export class OmniSharpServer { // If there's more than one launch target, we start the server if one of the targets // matches the preferred path. Otherwise, we fire the "MultipleLaunchTargets" event, // which is handled in status.ts to display the launch target selector. - if (launchTargets.length > 1 && preferredPath) { - - for (let launchTarget of launchTargets) { - if (launchTarget.target === preferredPath) { - // start preferred path - return this.restart(launchTarget); - } + if (preferredPath) { + const preferredLaunchTarget = launchTargets.find((a) => a.target === preferredPath); + if (preferredLaunchTarget) { + return this.restart(preferredLaunchTarget); } - - this._fireEvent(Events.MultipleLaunchTargets, launchTargets); - return Promise.reject(undefined); } - // If there's only one target, just start - return this.restart(launchTargets[0]); + this._fireEvent(Events.MultipleLaunchTargets, launchTargets); + return showProjectSelector(this, launchTargets); }); } diff --git a/test/integrationTests/launcher.test.ts b/test/integrationTests/launcher.test.ts index 62590501f..7431ad11a 100644 --- a/test/integrationTests/launcher.test.ts +++ b/test/integrationTests/launcher.test.ts @@ -5,20 +5,10 @@ import * as vscode from 'vscode'; import { assert } from "chai"; -import { resourcesToLaunchTargets, vsls, vslsTarget } from "../../src/omnisharp/launcher"; -import { isSlnWithGenerator } from './integrationHelpers'; - -const chai = require('chai'); -chai.use(require('chai-arrays')); -chai.use(require('chai-fs')); +import { LaunchTargetKind, resourcesAndFolderMapToLaunchTargets, resourcesToLaunchTargets, vsls, vslsTarget } from "../../src/omnisharp/launcher"; suite(`launcher:`, () => { - - suiteSetup(async function () { - if (isSlnWithGenerator(vscode.workspace)) { - this.skip(); - } - }); + const workspaceFolders: vscode.WorkspaceFolder[] = [{ uri: vscode.Uri.parse('/'), name: "root", index: 0 }]; test(`Returns the LiveShare launch target when processing vsls resources`, () => { const testResources: vscode.Uri[] = [ @@ -39,10 +29,28 @@ suite(`launcher:`, () => { vscode.Uri.parse(`/test/test.csproj`), vscode.Uri.parse(`/test/Program.cs`), ]; + const folderMap = new Map([[0, testResources]]); - const launchTargets = resourcesToLaunchTargets(testResources); + const launchTargets = resourcesAndFolderMapToLaunchTargets(testResources, workspaceFolders, folderMap); const liveShareTarget = launchTargets.find(target => target === vslsTarget); assert.notExists(liveShareTarget, "Launch targets contained the Visual Studio Live Share target."); }); + + test(`Returns a Solution and Project target`, () => { + const testResources: vscode.Uri[] = [ + vscode.Uri.parse(`/test.sln`), + vscode.Uri.parse(`/test/test.csproj`), + vscode.Uri.parse(`/test/Program.cs`), + ]; + const folderMap = new Map([[0, testResources]]); + + const launchTargets = resourcesAndFolderMapToLaunchTargets(testResources, workspaceFolders, folderMap); + + const solutionTarget = launchTargets.find(target => target.kind === LaunchTargetKind.Solution && target.label === "test.sln"); + assert.exists(solutionTarget, "Launch targets did not include `/test.sln`"); + + const projectTarget = launchTargets.find(target => target.kind === LaunchTargetKind.Project && target.label === "test.csproj"); + assert.exists(projectTarget, "Launch targets did not include `/test/test.csproj`"); + }); });