diff --git a/package-lock.json b/package-lock.json index 1d31d2691f..c32b2a420d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,15 @@ "@types/chai": "4.1.2" } }, + "@types/chai-as-promised": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.0.tgz", + "integrity": "sha512-MFiW54UOSt+f2bRw8J7LgQeIvE/9b4oGvwU7XW30S9QGAiHGnU/fmiOprsyMkdmH2rl8xSPc0/yrQw8juXU6bQ==", + "dev": true, + "requires": { + "@types/chai": "4.1.2" + } + }, "@types/chai-string": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@types/chai-string/-/chai-string-1.4.0.tgz", @@ -4233,11 +4242,6 @@ "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=", "dev": true }, - "lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" - }, "lodash.escape": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", @@ -8529,6 +8533,11 @@ "glob": "7.1.2" } }, + "rx": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", + "integrity": "sha1-pfE/957zt0D+MKqAP7CfmIBdR4I=" + }, "rxjs": { "version": "5.5.6", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.6.tgz", diff --git a/package.json b/package.json index 486fae3646..15c2b77dc9 100644 --- a/package.json +++ b/package.json @@ -69,10 +69,10 @@ "http-proxy-agent": "^2.0.0", "https-proxy-agent": "^2.1.1", "jsonc-parser": "^1.0.0", - "lodash.debounce": "^4.0.8", "mkdirp": "^0.5.1", "open": "*", "request-light": "^0.2.0", + "rx": "^4.1.0", "rxjs": "^5.5.6", "semver": "*", "tmp": "0.0.33", @@ -83,6 +83,7 @@ "devDependencies": { "@types/chai": "^4.1.2", "@types/chai-arrays": "1.0.2", + "@types/chai-as-promised": "^7.1.0", "@types/chai-string": "^1.4.0", "@types/fs-extra": "^5.0.1", "@types/mkdirp": "^0.5.2", diff --git a/src/features/commands.ts b/src/features/commands.ts index e976599ea1..faf4cad87c 100644 --- a/src/features/commands.ts +++ b/src/features/commands.ts @@ -24,7 +24,14 @@ export default function registerCommands(server: OmniSharpServer, eventStream: E let d1 = vscode.commands.registerCommand('o.restart', () => restartOmniSharp(server)); let d2 = vscode.commands.registerCommand('o.pickProjectAndStart', () => pickProjectAndStart(server)); let d3 = vscode.commands.registerCommand('o.showOutput', () => eventStream.post(new CommandShowOutput())); - let d4 = vscode.commands.registerCommand('dotnet.restore', () => dotnetRestoreAllProjects(server, eventStream)); + let d4 = vscode.commands.registerCommand('dotnet.restore', fileName => { + if (fileName) { + dotnetRestoreForProject(server, fileName, eventStream); + } + else { + dotnetRestoreAllProjects(server, eventStream); + } + }); // register empty handler for csharp.installDebugger // running the command activates the extension, which is all we need for installation to kickoff diff --git a/src/features/status.ts b/src/features/status.ts deleted file mode 100644 index a5c8d26471..0000000000 --- a/src/features/status.ts +++ /dev/null @@ -1,270 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import * as vscode from 'vscode'; -import * as serverUtils from '../omnisharp/utils'; -import {OmniSharpServer} from '../omnisharp/server'; -import {dotnetRestoreForProject} from './commands'; -import {basename} from 'path'; -import { OmnisharpServerOnServerError, OmnisharpServerOnError, OmnisharpServerMsBuildProjectDiagnostics, OmnisharpServerUnresolvedDependencies, OmnisharpServerOnStdErr } from '../omnisharp/loggingEvents'; -import { EventStream } from '../EventStream'; - -const debounce = require('lodash.debounce'); - -export default function reportStatus(server: OmniSharpServer, eventStream: EventStream) { - return vscode.Disposable.from( - reportServerStatus(server, eventStream), - forwardOutput(server, eventStream), - reportDocumentStatus(server)); -} - -// --- document status - -let defaultSelector: vscode.DocumentSelector = [ - 'csharp', // c#-files OR - { pattern: '**/project.json' }, // project.json-files OR - { pattern: '**/*.sln' }, // any solution file OR - { pattern: '**/*.csproj' }, // an csproj file - { pattern: '**/*.csx' }, // C# script - { pattern: '**/*.cake' } // Cake script -]; - -class Status { - - selector: vscode.DocumentSelector; - text: string; - command: string; - color: string; - - constructor(selector: vscode.DocumentSelector) { - this.selector = selector; - } -} - -export function reportDocumentStatus(server: OmniSharpServer): vscode.Disposable { - - let disposables: vscode.Disposable[] = []; - let localDisposables: vscode.Disposable[]; - - let entry = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, Number.MIN_VALUE); - let defaultStatus = new Status(defaultSelector); - let projectStatus: Status; - - function render() { - - if (!vscode.window.activeTextEditor) { - entry.hide(); - return; - } - - let document = vscode.window.activeTextEditor.document; - let status: Status; - - if (projectStatus && vscode.languages.match(projectStatus.selector, document)) { - status = projectStatus; - } else if (defaultStatus.text && vscode.languages.match(defaultStatus.selector, document)) { - status = defaultStatus; - } - - if (status) { - entry.text = status.text; - entry.command = status.command; - entry.color = status.color; - entry.show(); - return; - } - - entry.hide(); - } - - disposables.push(vscode.window.onDidChangeActiveTextEditor(render)); - - disposables.push(server.onServerError(err => { - defaultStatus.text = '$(flame) Error starting OmniSharp'; - defaultStatus.command = 'o.showOutput'; - defaultStatus.color = ''; - render(); - })); - - disposables.push(server.onMultipleLaunchTargets(targets => { - defaultStatus.text = '$(flame) Select project'; - defaultStatus.command = 'o.pickProjectAndStart'; - defaultStatus.color = 'rgb(90, 218, 90)'; - render(); - })); - - disposables.push(server.onBeforeServerInstall(() => { - defaultStatus.text = '$(flame) Installing OmniSharp...'; - defaultStatus.command = 'o.showOutput'; - defaultStatus.color = ''; - render(); - })); - - disposables.push(server.onBeforeServerStart(path => { - defaultStatus.text = '$(flame) Starting...'; - defaultStatus.command = 'o.showOutput'; - defaultStatus.color = ''; - render(); - })); - - disposables.push(server.onServerStop(() => { - projectStatus = undefined; - defaultStatus.text = undefined; - - if (localDisposables) { - vscode.Disposable.from(...localDisposables).dispose(); - } - - localDisposables = undefined; - })); - - disposables.push(server.onServerStart(path => { - localDisposables = []; - - defaultStatus.text = '$(flame) Running'; - defaultStatus.command = 'o.pickProjectAndStart'; - defaultStatus.color = ''; - render(); - - function updateProjectInfo() { - serverUtils.requestWorkspaceInformation(server).then(info => { - - interface Project { - Path: string; - SourceFiles: string[]; - } - - let fileNames: vscode.DocumentFilter[] = []; - let label: string; - - function addProjectFileNames(project: Project) { - fileNames.push({ pattern: project.Path }); - - if (project.SourceFiles) { - for (let sourceFile of project.SourceFiles) { - fileNames.push({ pattern: sourceFile }); - } - } - } - - function addDnxOrDotNetProjects(projects: Project[]) { - let count = 0; - - for (let project of projects) { - count += 1; - addProjectFileNames(project); - } - - if (!label) { - if (count === 1) { - label = basename(projects[0].Path); //workspace.getRelativePath(info.Dnx.Projects[0].Path); - } - else { - label = `${count} projects`; - } - } - } - - // show sln-file if applicable - if (info.MsBuild && info.MsBuild.SolutionPath) { - label = basename(info.MsBuild.SolutionPath); //workspace.getRelativePath(info.MsBuild.SolutionPath); - fileNames.push({ pattern: info.MsBuild.SolutionPath }); - - for (let project of info.MsBuild.Projects) { - addProjectFileNames(project); - } - } - - // show .NET Core projects if applicable - if (info.DotNet) { - addDnxOrDotNetProjects(info.DotNet.Projects); - } - - // set project info - projectStatus = new Status(fileNames); - projectStatus.text = '$(flame) ' + label; - projectStatus.command = 'o.pickProjectAndStart'; - - // default is to change project - defaultStatus.text = '$(flame) Switch projects'; - defaultStatus.command = 'o.pickProjectAndStart'; - render(); - }); - } - - // Don't allow the same request to slam the server within a "short" window - let debouncedUpdateProjectInfo = debounce(updateProjectInfo, 1500, { leading: true }); - localDisposables.push(server.onProjectAdded(debouncedUpdateProjectInfo)); - localDisposables.push(server.onProjectChange(debouncedUpdateProjectInfo)); - localDisposables.push(server.onProjectRemoved(debouncedUpdateProjectInfo)); - })); - - return vscode.Disposable.from(...disposables); -} - - -// ---- server status - -export function reportServerStatus(server: OmniSharpServer, eventStream: EventStream): vscode.Disposable{ - - - let d0 = server.onServerError(err => { - eventStream.post(new OmnisharpServerOnServerError('[ERROR] ' + err)); - }); - - let d1 = server.onError(message => { - eventStream.post(new OmnisharpServerOnError(message)); - - showMessageSoon(); - }); - - let d2 = server.onMsBuildProjectDiagnostics(message => { - eventStream.post(new OmnisharpServerMsBuildProjectDiagnostics(message)); - - if (message.Errors.length > 0) { - showMessageSoon(); - } - }); - - let d3 = server.onUnresolvedDependencies(message => { - eventStream.post(new OmnisharpServerUnresolvedDependencies(message)); - - let csharpConfig = vscode.workspace.getConfiguration('csharp'); - if (!csharpConfig.get('suppressDotnetRestoreNotification')) { - let info = `There are unresolved dependencies from '${vscode.workspace.asRelativePath(message.FileName) }'. Please execute the restore command to continue.`; - - return vscode.window.showInformationMessage(info, 'Restore').then(value => { - if (value) { - dotnetRestoreForProject(server, message.FileName, eventStream); - } - }); - } - }); - - return vscode.Disposable.from(d0, d1, d2, d3); -} - -// show user message -let _messageHandle: NodeJS.Timer; -function showMessageSoon() { - clearTimeout(_messageHandle); - _messageHandle = setTimeout(function() { - - let message = "Some projects have trouble loading. Please review the output for more details."; - vscode.window.showWarningMessage(message, { title: "Show Output", command: 'o.showOutput' }).then(value => { - if (value) { - vscode.commands.executeCommand(value.command); - } - }); - }, 1500); -} - -// --- mirror output in channel - -function forwardOutput(server: OmniSharpServer, eventStream: EventStream) { - return vscode.Disposable.from( - server.onStderr(message => eventStream.post(new OmnisharpServerOnStdErr(message)))); -} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 48418d5e31..e56848a2db 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,20 +7,26 @@ import * as OmniSharp from './omnisharp/extension'; import * as coreclrdebug from './coreclr-debug/activate'; import * as util from './common'; import * as vscode from 'vscode'; + +import { ActivationFailure, ActiveTextEditorChanged } from './omnisharp/loggingEvents'; +import { WarningMessageObserver } from './observers/WarningMessageObserver'; import { CSharpExtDownloader } from './CSharpExtDownloader'; -import { PlatformInformation } from './platform'; -import TelemetryReporter from 'vscode-extension-telemetry'; -import { addJSONProviders } from './features/json/jsonContributions'; import { CsharpChannelObserver } from './observers/CsharpChannelObserver'; import { CsharpLoggerObserver } from './observers/CsharpLoggerObserver'; -import { OmnisharpLoggerObserver } from './observers/OmnisharpLoggerObserver'; import { DotNetChannelObserver } from './observers/DotnetChannelObserver'; -import { TelemetryObserver } from './observers/TelemetryObserver'; -import { OmnisharpChannelObserver } from './observers/OmnisharpChannelObserver'; import { DotnetLoggerObserver } from './observers/DotnetLoggerObserver'; -import { OmnisharpDebugModeLoggerObserver } from './observers/OmnisharpDebugModeLoggerObserver'; -import { ActivationFailure } from './omnisharp/loggingEvents'; import { EventStream } from './EventStream'; +import { InformationMessageObserver } from './observers/InformationMessageObserver'; +import { OmnisharpChannelObserver } from './observers/OmnisharpChannelObserver'; +import { OmnisharpDebugModeLoggerObserver } from './observers/OmnisharpDebugModeLoggerObserver'; +import { OmnisharpLoggerObserver } from './observers/OmnisharpLoggerObserver'; +import { OmnisharpStatusBarObserver } from './observers/OmnisharpStatusBarObserver'; +import { PlatformInformation } from './platform'; +import { StatusBarItemAdapter } from './statusBarItemAdapter'; +import { TelemetryObserver } from './observers/TelemetryObserver'; +import TelemetryReporter from 'vscode-extension-telemetry'; +import { addJSONProviders } from './features/json/jsonContributions'; +import { ProjectStatusBarObserver } from './observers/ProjectStatusBarObserver'; export async function activate(context: vscode.ExtensionContext): Promise<{ initializationFinished: Promise }> { @@ -31,34 +37,47 @@ export async function activate(context: vscode.ExtensionContext): Promise<{ init const reporter = new TelemetryReporter(extensionId, extensionVersion, aiKey); util.setExtensionPath(extension.extensionPath); - + + const eventStream = new EventStream(); + let dotnetChannel = vscode.window.createOutputChannel('.NET'); let dotnetChannelObserver = new DotNetChannelObserver(dotnetChannel); let dotnetLoggerObserver = new DotnetLoggerObserver(dotnetChannel); + eventStream.subscribe(dotnetChannelObserver.post); + eventStream.subscribe(dotnetLoggerObserver.post); let csharpChannel = vscode.window.createOutputChannel('C#'); let csharpchannelObserver = new CsharpChannelObserver(csharpChannel); let csharpLogObserver = new CsharpLoggerObserver(csharpChannel); + eventStream.subscribe(csharpchannelObserver.post); + eventStream.subscribe(csharpLogObserver.post); let omnisharpChannel = vscode.window.createOutputChannel('OmniSharp Log'); let omnisharpLogObserver = new OmnisharpLoggerObserver(omnisharpChannel); let omnisharpChannelObserver = new OmnisharpChannelObserver(omnisharpChannel); + eventStream.subscribe(omnisharpLogObserver.post); + eventStream.subscribe(omnisharpChannelObserver.post); - const eventStream = new EventStream(); - eventStream.subscribe(dotnetChannelObserver.post); - eventStream.subscribe(dotnetLoggerObserver.post); + let warningMessageObserver = new WarningMessageObserver(vscode); + eventStream.subscribe(warningMessageObserver.post); - eventStream.subscribe(csharpchannelObserver.post); - eventStream.subscribe(csharpLogObserver.post); + let informationMessageObserver = new InformationMessageObserver(vscode); + eventStream.subscribe(informationMessageObserver.post); + + let omnisharpStatusBar = new StatusBarItemAdapter(vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, Number.MIN_VALUE)); + let omnisharpStatusBarObserver = new OmnisharpStatusBarObserver(omnisharpStatusBar); + eventStream.subscribe(omnisharpStatusBarObserver.post); + + let projectStatusBar = new StatusBarItemAdapter(vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left)); + let projectStatusBarObserver = new ProjectStatusBarObserver(projectStatusBar); + eventStream.subscribe(projectStatusBarObserver.post); - eventStream.subscribe(omnisharpLogObserver.post); - eventStream.subscribe(omnisharpChannelObserver.post); const debugMode = false; if (debugMode) { let omnisharpDebugModeLoggerObserver = new OmnisharpDebugModeLoggerObserver(omnisharpChannel); eventStream.subscribe(omnisharpDebugModeLoggerObserver.post); } - + let platformInfo: PlatformInformation; try { platformInfo = await PlatformInformation.GetCurrent(); @@ -77,6 +96,9 @@ export async function activate(context: vscode.ExtensionContext): Promise<{ init // register JSON completion & hover providers for project.json context.subscriptions.push(addJSONProviders()); + context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(() => { + eventStream.post(new ActiveTextEditorChanged()); + })); let coreClrDebugPromise = Promise.resolve(); if (runtimeDependenciesExist) { diff --git a/src/observers/BaseStatusBarItemObserver.ts b/src/observers/BaseStatusBarItemObserver.ts new file mode 100644 index 0000000000..fa47c107a4 --- /dev/null +++ b/src/observers/BaseStatusBarItemObserver.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { StatusBarItem } from '../vscodeAdapter'; +import { BaseEvent } from '../omnisharp/loggingEvents'; + +export abstract class BaseStatusBarItemObserver { + + constructor(private statusBarItem: StatusBarItem) { + } + + public SetAndShowStatusBar(text: string, command: string, color?: string) { + this.statusBarItem.text = text; + this.statusBarItem.command = command; + this.statusBarItem.color = color; + this.statusBarItem.show(); + } + + public ResetAndHideStatusBar() { + this.statusBarItem.text = undefined; + this.statusBarItem.command = undefined; + this.statusBarItem.color = undefined; + this.statusBarItem.hide(); + } + + abstract post: (event: BaseEvent) => void; +} \ No newline at end of file diff --git a/src/observers/InformationMessageObserver.ts b/src/observers/InformationMessageObserver.ts new file mode 100644 index 0000000000..6fbf4093c7 --- /dev/null +++ b/src/observers/InformationMessageObserver.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as ObservableEvent from "../omnisharp/loggingEvents"; +import { vscode } from '../vscodeAdapter'; + +export class InformationMessageObserver { + constructor(private vscode: vscode) { + } + + public post = (event: ObservableEvent.BaseEvent) => { + switch (event.constructor.name) { + case ObservableEvent.OmnisharpServerUnresolvedDependencies.name: + this.handleOmnisharpServerUnresolvedDependencies(event); + break; + } + } + + private async handleOmnisharpServerUnresolvedDependencies(event: ObservableEvent.OmnisharpServerUnresolvedDependencies) { + let csharpConfig = this.vscode.workspace.getConfiguration('csharp'); + if (!csharpConfig.get('suppressDotnetRestoreNotification')) { + let info = `There are unresolved dependencies from '${this.vscode.workspace.asRelativePath(event.unresolvedDependencies.FileName)}'. Please execute the restore command to continue.`; + let value = await this.vscode.window.showInformationMessage(info, 'Restore'); + if (value) { + this.vscode.commands.executeCommand('dotnet.restore', event.unresolvedDependencies.FileName); + } + } + } +} diff --git a/src/observers/OmnisharpLoggerObserver.ts b/src/observers/OmnisharpLoggerObserver.ts index 2ccbe6b809..37ffdafc40 100644 --- a/src/observers/OmnisharpLoggerObserver.ts +++ b/src/observers/OmnisharpLoggerObserver.ts @@ -24,7 +24,7 @@ export class OmnisharpLoggerObserver extends BaseLoggerObserver { this.logger.appendLine((event).message); break; case OmnisharpServerOnServerError.name: - this.logger.appendLine((event).message); + this.handleOmnisharpServerOnServerError(event); break; case OmnisharpServerOnError.name: this.handleOmnisharpServerOnError(event); @@ -41,6 +41,10 @@ export class OmnisharpLoggerObserver extends BaseLoggerObserver { } } + private handleOmnisharpServerOnServerError(event: OmnisharpServerOnServerError) { + this.logger.appendLine('[ERROR] ' + event.err); + } + private handleOmnisharpInitialisation(event: OmnisharpInitialisation) { this.logger.appendLine(`Starting OmniSharp server at ${event.timeStamp.toLocaleString()}`); this.logger.increaseIndent(); diff --git a/src/observers/OmnisharpStatusBarObserver.ts b/src/observers/OmnisharpStatusBarObserver.ts new file mode 100644 index 0000000000..3258bb9666 --- /dev/null +++ b/src/observers/OmnisharpStatusBarObserver.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { OmnisharpServerOnServerError, BaseEvent, OmnisharpOnBeforeServerInstall, OmnisharpOnBeforeServerStart, OmnisharpServerOnStop, OmnisharpServerOnStart } from "../omnisharp/loggingEvents"; +import { BaseStatusBarItemObserver } from './BaseStatusBarItemObserver'; + +export class OmnisharpStatusBarObserver extends BaseStatusBarItemObserver { + public post = (event: BaseEvent) => { + switch (event.constructor.name) { + case OmnisharpServerOnServerError.name: + this.SetAndShowStatusBar('$(flame) Error starting OmniSharp', 'o.showOutput', ''); + break; + case OmnisharpOnBeforeServerInstall.name: + this.SetAndShowStatusBar('$(flame) Installing OmniSharp...', 'o.showOutput', ''); + break; + case OmnisharpOnBeforeServerStart.name: + this.SetAndShowStatusBar('$(flame) Starting...', 'o.showOutput', ''); + break; + case OmnisharpServerOnStop.name: + this.ResetAndHideStatusBar(); + break; + case OmnisharpServerOnStart.name: + this.SetAndShowStatusBar('$(flame) Running', 'o.showOutput', ''); + break; + } + } +} + diff --git a/src/observers/ProjectStatusBarObserver.ts b/src/observers/ProjectStatusBarObserver.ts new file mode 100644 index 0000000000..fd37b7b20e --- /dev/null +++ b/src/observers/ProjectStatusBarObserver.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { basename } from 'path'; +import { BaseEvent, OmnisharpOnMultipleLaunchTargets, WorkspaceInformationUpdated, OmnisharpServerOnStop } from "../omnisharp/loggingEvents"; +import { BaseStatusBarItemObserver } from './BaseStatusBarItemObserver'; + +export class ProjectStatusBarObserver extends BaseStatusBarItemObserver { + + public post = (event: BaseEvent) => { + switch (event.constructor.name) { + case OmnisharpOnMultipleLaunchTargets.name: + this.SetAndShowStatusBar('$(file-submodule) Select project', 'o.pickProjectAndStart', 'rgb(90, 218, 90)'); + break; + case OmnisharpServerOnStop.name: + this.ResetAndHideStatusBar(); + break; + case WorkspaceInformationUpdated.name: + this.handleWorkspaceInformationUpdated(event); + } + } + + 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); + this.SetAndShowStatusBar('$(file-directory) ' + label, 'o.pickProjectAndStart'); + } + else { + this.ResetAndHideStatusBar(); + } + } +} \ No newline at end of file diff --git a/src/observers/WarningMessageObserver.ts b/src/observers/WarningMessageObserver.ts new file mode 100644 index 0000000000..e6f867c8b6 --- /dev/null +++ b/src/observers/WarningMessageObserver.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MessageItem, vscode } from '../vscodeAdapter'; +import { Scheduler, Subject } from 'rx'; +import { BaseEvent, OmnisharpServerOnError, OmnisharpServerMsBuildProjectDiagnostics } from "../omnisharp/loggingEvents"; + +export interface MessageItemWithCommand extends MessageItem { + command: string; +} + +export class WarningMessageObserver { + private warningMessageDebouncer: Subject; + + constructor(private vscode: vscode, scheduler?: Scheduler) { + this.warningMessageDebouncer = new Subject(); + this.warningMessageDebouncer.debounce(1500, scheduler).subscribe(async event => { + let message = "Some projects have trouble loading. Please review the output for more details."; + let value = await this.vscode.window.showWarningMessage(message, { title: "Show Output", command: 'o.showOutput' }); + if (value) { + await this.vscode.commands.executeCommand(value.command); + } + }); + } + + public post = (event: BaseEvent) => { + switch (event.constructor.name) { + case OmnisharpServerOnError.name: + this.warningMessageDebouncer.onNext(event); + break; + case OmnisharpServerMsBuildProjectDiagnostics.name: + this.handleOmnisharpServerMsBuildProjectDiagnostics(event); + break; + } + } + + private handleOmnisharpServerMsBuildProjectDiagnostics(event: OmnisharpServerMsBuildProjectDiagnostics) { + if (event.diagnostics.Errors.length > 0) { + this.warningMessageDebouncer.onNext(event); + } + } +} \ No newline at end of file diff --git a/src/omnisharp/extension.ts b/src/omnisharp/extension.ts index 12e68b4f05..5e9884c865 100644 --- a/src/omnisharp/extension.ts +++ b/src/omnisharp/extension.ts @@ -28,7 +28,6 @@ import TestManager from '../features/dotnetTest'; import WorkspaceSymbolProvider from '../features/workspaceSymbolProvider'; import forwardChanges from '../features/changeForwarding'; import registerCommands from '../features/commands'; -import reportStatus from '../features/status'; import { PlatformInformation } from '../platform'; import { ProjectJsonDeprecatedWarning, OmnisharpStart } from './loggingEvents'; import { EventStream } from '../EventStream'; @@ -87,7 +86,6 @@ export function activate(context: vscode.ExtensionContext, eventStream: EventStr })); disposables.push(registerCommands(server, eventStream,platformInfo)); - disposables.push(reportStatus(server, eventStream)); if (!context.workspaceState.get('assetPromptDisabled')) { disposables.push(server.onServerStart(() => { diff --git a/src/omnisharp/loggingEvents.ts b/src/omnisharp/loggingEvents.ts index 9e89be2d66..df1fc2c732 100644 --- a/src/omnisharp/loggingEvents.ts +++ b/src/omnisharp/loggingEvents.ts @@ -6,11 +6,12 @@ import { PlatformInformation } from "../platform"; import { Request } from "./requestQueue"; import * as protocol from './protocol'; +import { LaunchTarget } from "./launcher"; -export interface BaseEvent{ +export interface BaseEvent { } -export class TelemetryEventWithMeasures implements BaseEvent{ +export class TelemetryEventWithMeasures implements BaseEvent { constructor(public eventName: string, public measures: { [key: string]: number }) { } } @@ -21,75 +22,87 @@ export class OmnisharpDelayTrackerEventMeasures extends TelemetryEventWithMeasur export class OmnisharpStart extends TelemetryEventWithMeasures { } -export class OmnisharpInitialisation implements BaseEvent{ +export class OmnisharpInitialisation implements BaseEvent { constructor(public timeStamp: Date, public solutionPath: string) { } } -export class OmnisharpLaunch implements BaseEvent{ +export class OmnisharpLaunch implements BaseEvent { constructor(public usingMono: boolean, public command: string, public pid: number) { } } -export class PackageInstallation implements BaseEvent{ +export class PackageInstallation implements BaseEvent { constructor(public packageInfo: string) { } } -export class LogPlatformInfo implements BaseEvent{ +export class LogPlatformInfo implements BaseEvent { constructor(public info: PlatformInformation) { } } -export class InstallationProgress implements BaseEvent{ +export class InstallationProgress implements BaseEvent { constructor(public stage: string, public message: string) { } } -export class InstallationFailure implements BaseEvent{ - constructor(public stage: string,public error: any) { } +export class InstallationFailure implements BaseEvent { + constructor(public stage: string, public error: any) { } } -export class DownloadProgress implements BaseEvent{ +export class DownloadProgress implements BaseEvent { constructor(public downloadPercentage: number) { } } -export class OmnisharpFailure implements BaseEvent{ +export class OmnisharpFailure implements BaseEvent { constructor(public message: string, public error: Error) { } } -export class OmnisharpRequestMessage implements BaseEvent{ +export class OmnisharpRequestMessage implements BaseEvent { constructor(public request: Request, public id: number) { } } -export class TestExecutionCountReport implements BaseEvent{ +export class TestExecutionCountReport implements BaseEvent { constructor(public debugCounts: { [testFrameworkName: string]: number }, public runCounts: { [testFrameworkName: string]: number }) { } } -export class OmnisharpServerOnError implements BaseEvent{ +export class OmnisharpServerOnError implements BaseEvent { constructor(public errorMessage: protocol.ErrorMessage) { } } -export class OmnisharpServerMsBuildProjectDiagnostics implements BaseEvent{ +export class OmnisharpServerMsBuildProjectDiagnostics implements BaseEvent { constructor(public diagnostics: protocol.MSBuildProjectDiagnostics) { } } -export class OmnisharpServerUnresolvedDependencies implements BaseEvent{ +export class OmnisharpServerUnresolvedDependencies implements BaseEvent { constructor(public unresolvedDependencies: protocol.UnresolvedDependenciesMessage) { } } -export class OmnisharpServerEnqueueRequest implements BaseEvent{ +export class OmnisharpServerEnqueueRequest implements BaseEvent { constructor(public name: string, public command: string) { } } -export class OmnisharpServerDequeueRequest implements BaseEvent{ +export class OmnisharpServerDequeueRequest implements BaseEvent { constructor(public name: string, public command: string, public id: number) { } } -export class OmnisharpServerProcessRequestStart implements BaseEvent{ +export class OmnisharpServerProcessRequestStart implements BaseEvent { constructor(public name: string) { } } -export class OmnisharpEventPacketReceived implements BaseEvent{ +export class OmnisharpEventPacketReceived implements BaseEvent { constructor(public logLevel: string, public name: string, public message: string) { } } -export class EventWithMessage implements BaseEvent{ +export class OmnisharpServerOnServerError implements BaseEvent { + constructor(public err: any) { } +} + +export class OmnisharpOnMultipleLaunchTargets implements BaseEvent { + constructor(public targets: LaunchTarget[]) { } +} + +export class WorkspaceInformationUpdated implements BaseEvent { + constructor(public info: protocol.WorkspaceInformationResponse) { } +} + +export class EventWithMessage implements BaseEvent { constructor(public message: string) { } } @@ -103,14 +116,18 @@ export class DownloadSuccess extends EventWithMessage { } export class DownloadFailure extends EventWithMessage { } export class OmnisharpServerOnStdErr extends EventWithMessage { } export class OmnisharpServerMessage extends EventWithMessage { } -export class OmnisharpServerOnServerError extends EventWithMessage { } export class OmnisharpServerVerboseMessage extends EventWithMessage { } - -export class ActivationFailure implements BaseEvent{ } -export class CommandShowOutput implements BaseEvent{ } -export class DebuggerNotInstalledFailure implements BaseEvent{ } -export class CommandDotNetRestoreStart implements BaseEvent{ } -export class InstallationSuccess implements BaseEvent{ } -export class OmnisharpServerProcessRequestComplete implements BaseEvent{ } -export class ProjectJsonDeprecatedWarning implements BaseEvent{ } \ No newline at end of file +export class ProjectModified implements BaseEvent { } +export class ActivationFailure implements BaseEvent { } +export class CommandShowOutput implements BaseEvent { } +export class DebuggerNotInstalledFailure implements BaseEvent { } +export class CommandDotNetRestoreStart implements BaseEvent { } +export class InstallationSuccess implements BaseEvent { } +export class OmnisharpServerProcessRequestComplete implements BaseEvent { } +export class ProjectJsonDeprecatedWarning implements BaseEvent { } +export class OmnisharpOnBeforeServerStart implements BaseEvent { } +export class OmnisharpOnBeforeServerInstall implements BaseEvent { } +export class ActiveTextEditorChanged implements BaseEvent { } +export class OmnisharpServerOnStop implements BaseEvent { } +export class OmnisharpServerOnStart implements BaseEvent { } \ No newline at end of file diff --git a/src/omnisharp/server.ts b/src/omnisharp/server.ts index 2baf353582..a00d44a66f 100644 --- a/src/omnisharp/server.ts +++ b/src/omnisharp/server.ts @@ -7,6 +7,7 @@ import * as path from 'path'; import * as protocol from './protocol'; import * as utils from '../common'; import * as vscode from 'vscode'; +import * as serverUtils from '../omnisharp/utils'; import { ChildProcess, exec } from 'child_process'; import { LaunchTarget, findLaunchTargets } from './launcher'; @@ -20,8 +21,9 @@ import { PlatformInformation } from '../platform'; import { launchOmniSharp } from './launcher'; import { setTimeout } from 'timers'; import { OmnisharpDownloader } from './OmnisharpDownloader'; -import { OmnisharpDelayTrackerEventMeasures, OmnisharpFailure, OmnisharpInitialisation, OmnisharpLaunch, OmnisharpServerMessage, OmnisharpServerVerboseMessage, OmnisharpEventPacketReceived, OmnisharpRequestMessage } from './loggingEvents'; +import * as ObservableEvents from './loggingEvents'; import { EventStream } from '../EventStream'; +import { Disposable, CompositeDisposable, Subject } from 'rx'; enum ServerState { Starting, @@ -68,9 +70,8 @@ const latestVersionFileServerPath = 'releases/versioninfo.txt'; export class OmniSharpServer { private static _nextId = 1; - private _readLine: ReadLine; - private _disposables: vscode.Disposable[] = []; + private _disposables: CompositeDisposable; private _delayTrackers: { [requestName: string]: DelayTracker }; private _telemetryIntervalId: NodeJS.Timer = undefined; @@ -84,12 +85,16 @@ export class OmniSharpServer { private _omnisharpManager: OmnisharpManager; private eventStream: EventStream; + private updateProjectDebouncer = new Subject(); + private firstUpdateProject: boolean; constructor(eventStream: EventStream, packageJSON: any, platformInfo: PlatformInformation) { this.eventStream = eventStream; this._requestQueue = new RequestQueueCollection(this.eventStream, 8, request => this._makeRequest(request)); let downloader = new OmnisharpDownloader(this.eventStream, packageJSON, platformInfo); this._omnisharpManager = new OmnisharpManager(downloader, platformInfo); + this.updateProjectDebouncer.debounce(1500).subscribe((event) => { this.updateProjectInfo(); }); + this.firstUpdateProject = true; } public isRunning(): boolean { @@ -134,7 +139,7 @@ export class OmniSharpServer { const measures = tracker.getMeasures(); tracker.clearMeasures(); - this.eventStream.post(new OmnisharpDelayTrackerEventMeasures(eventName, measures)); + this.eventStream.post(new ObservableEvents.OmnisharpDelayTrackerEventMeasures(eventName, measures)); } } } @@ -218,10 +223,10 @@ export class OmniSharpServer { return this._addListener(Events.Started, listener); } - private _addListener(event: string, listener: (e: any) => any, thisArg?: any): vscode.Disposable { + private _addListener(event: string, listener: (e: any) => any, thisArg?: any): Disposable { listener = thisArg ? listener.bind(thisArg) : listener; this._eventBus.addListener(event, listener); - return new vscode.Disposable(() => this._eventBus.removeListener(event, listener)); + return Disposable.create(() => this._eventBus.removeListener(event, listener)); } protected _fireEvent(event: string, args: any): void { @@ -231,6 +236,55 @@ export class OmniSharpServer { // --- start, stop, and connect private async _start(launchTarget: LaunchTarget): Promise { + + let disposables = new CompositeDisposable(); + + disposables.add(this.onServerError(err => + this.eventStream.post(new ObservableEvents.OmnisharpServerOnServerError(err)) + )); + + disposables.add(this.onError((message: protocol.ErrorMessage) => + this.eventStream.post(new ObservableEvents.OmnisharpServerOnError(message)) + )); + + disposables.add(this.onMsBuildProjectDiagnostics((message: protocol.MSBuildProjectDiagnostics) => + this.eventStream.post(new ObservableEvents.OmnisharpServerMsBuildProjectDiagnostics(message)) + )); + + disposables.add(this.onUnresolvedDependencies((message: protocol.UnresolvedDependenciesMessage) => + this.eventStream.post(new ObservableEvents.OmnisharpServerUnresolvedDependencies(message)) + )); + + disposables.add(this.onStderr((message: string) => + this.eventStream.post(new ObservableEvents.OmnisharpServerOnStdErr(message)) + )); + + disposables.add(this.onMultipleLaunchTargets((targets: LaunchTarget[]) => + this.eventStream.post(new ObservableEvents.OmnisharpOnMultipleLaunchTargets(targets)) + )); + + disposables.add(this.onBeforeServerInstall(() => + this.eventStream.post(new ObservableEvents.OmnisharpOnBeforeServerInstall()) + )); + + disposables.add(this.onBeforeServerStart(() => { + this.eventStream.post(new ObservableEvents.OmnisharpOnBeforeServerStart()); + })); + + disposables.add(this.onServerStop(() => + this.eventStream.post(new ObservableEvents.OmnisharpServerOnStop()) + )); + + disposables.add(this.onServerStart(() => { + this.eventStream.post(new ObservableEvents.OmnisharpServerOnStart()); + })); + + disposables.add(this.onProjectAdded(this.debounceUpdateProjectWithLeadingTrue)); + disposables.add(this.onProjectChange(this.debounceUpdateProjectWithLeadingTrue)); + disposables.add(this.onProjectRemoved(this.debounceUpdateProjectWithLeadingTrue)); + + this._disposables = disposables; + this._setState(ServerState.Starting); this._launchTarget = launchTarget; @@ -258,16 +312,16 @@ export class OmniSharpServer { launchPath = await this._omnisharpManager.GetOmnisharpPath(this._options.path, this._options.useMono, serverUrl, latestVersionFileServerPath, installPath, extensionPath); } catch (error) { - this.eventStream.post(new OmnisharpFailure(`Error occured in loading omnisharp from omnisharp.path\nCould not start the server due to ${error.toString()}`, error)); + this.eventStream.post(new ObservableEvents.OmnisharpFailure(`Error occured in loading omnisharp from omnisharp.path\nCould not start the server due to ${error.toString()}`, error)); return; } } - this.eventStream.post(new OmnisharpInitialisation(new Date(), solutionPath)); + this.eventStream.post(new ObservableEvents.OmnisharpInitialisation(new Date(), solutionPath)); this._fireEvent(Events.BeforeServerStart, solutionPath); return launchOmniSharp(cwd, args, launchPath).then(value => { - this.eventStream.post(new OmnisharpLaunch(value.usingMono, value.command, value.process.pid)); + this.eventStream.post(new ObservableEvents.OmnisharpLaunch(value.usingMono, value.command, value.process.pid)); this._serverProcess = value.process; this._delayTrackers = {}; @@ -286,11 +340,26 @@ export class OmniSharpServer { }); } - public stop(): Promise { + private debounceUpdateProjectWithLeadingTrue = () => { + // Call the updateProjectInfo directly if it is the first time, otherwise debounce the request + // This needs to be done so that we have a project information for the first incoming request - while (this._disposables.length) { - this._disposables.pop().dispose(); + if (this.firstUpdateProject) { + this.updateProjectInfo(); } + else { + this.updateProjectDebouncer.onNext(new ObservableEvents.ProjectModified()); + } + } + + private updateProjectInfo = async () => { + this.firstUpdateProject = false; + let info = await serverUtils.requestWorkspaceInformation(this); + //once we get the info, push the event into the event stream + this.eventStream.post(new ObservableEvents.WorkspaceInformationUpdated(info)); + } + + public stop(): Promise { let cleanupPromise: Promise; @@ -332,10 +401,16 @@ export class OmniSharpServer { }); } + let disposables = this._disposables; + this._disposables = null; + return cleanupPromise.then(() => { this._serverProcess = null; this._setState(ServerState.Stopped); this._fireEvent(Events.ServerStop, this); + if (disposables) { + disposables.dispose(); + } }); } @@ -470,7 +545,7 @@ export class OmniSharpServer { this._readLine.addListener('line', lineReceived); - this._disposables.push(new vscode.Disposable(() => { + this._disposables.add(Disposable.create(() => { this._readLine.removeListener('line', lineReceived); })); @@ -481,7 +556,7 @@ export class OmniSharpServer { line = line.trim(); if (line[0] !== '{') { - this.eventStream.post(new OmnisharpServerMessage(line)); + this.eventStream.post(new ObservableEvents.OmnisharpServerMessage(line)); return; } @@ -507,7 +582,7 @@ export class OmniSharpServer { this._handleEventPacket(packet); break; default: - this.eventStream.post(new OmnisharpServerMessage(`Unknown packet type: ${packet.Type}`)); + this.eventStream.post(new ObservableEvents.OmnisharpServerMessage(`Unknown packet type: ${packet.Type}`)); break; } } @@ -516,11 +591,11 @@ export class OmniSharpServer { const request = this._requestQueue.dequeue(packet.Command, packet.Request_seq); if (!request) { - this.eventStream.post(new OmnisharpServerMessage(`Received response for ${packet.Command} but could not find request.`)); + this.eventStream.post(new ObservableEvents.OmnisharpServerMessage(`Received response for ${packet.Command} but could not find request.`)); return; } - this.eventStream.post(new OmnisharpServerVerboseMessage(`handleResponse: ${packet.Command} (${packet.Request_seq})`)); + this.eventStream.post(new ObservableEvents.OmnisharpServerVerboseMessage(`handleResponse: ${packet.Command} (${packet.Request_seq})`)); if (packet.Success) { request.onSuccess(packet.Body); @@ -535,7 +610,7 @@ export class OmniSharpServer { private _handleEventPacket(packet: protocol.WireProtocol.EventPacket): void { if (packet.Event === 'log') { const entry = <{ LogLevel: string; Name: string; Message: string; }>packet.Body; - this.eventStream.post(new OmnisharpEventPacketReceived(entry.LogLevel, entry.Name, entry.Message)); + this.eventStream.post(new ObservableEvents.OmnisharpEventPacketReceived(entry.LogLevel, entry.Name, entry.Message)); } else { // fwd all other events @@ -553,7 +628,7 @@ export class OmniSharpServer { Arguments: request.data }; - this.eventStream.post(new OmnisharpRequestMessage(request, id)); + this.eventStream.post(new ObservableEvents.OmnisharpRequestMessage(request, id)); this._serverProcess.stdin.write(JSON.stringify(requestPacket) + '\n'); return id; } diff --git a/src/statusBarItemAdapter.ts b/src/statusBarItemAdapter.ts new file mode 100644 index 0000000000..f91d56054a --- /dev/null +++ b/src/statusBarItemAdapter.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscodeAdapter from './vscodeAdapter'; +import * as vscode from 'vscode'; + +export class StatusBarItemAdapter implements vscodeAdapter.StatusBarItem { + + get alignment(): vscodeAdapter.StatusBarAlignment{ + return this.statusBarItem.alignment; + } + + get priority(): number{ + return this.statusBarItem.priority; + } + + get text(): string{ + return this.statusBarItem.text; + } + + set text(value: string) { + this.statusBarItem.text = value; + } + + get tooltip(): string{ + return this.statusBarItem.tooltip; + } + + set tooltip(value: string){ + this.statusBarItem.tooltip = value; + } + + get color(): string{ + return this.statusBarItem.color as string; + } + + set color(value: string) { + this.statusBarItem.color = value; + } + + get command(): string{ + return this.statusBarItem.command; + } + + set command(value: string) { + this.statusBarItem.command = value; + } + + show(): void { + this.statusBarItem.show(); + } + + hide(): void { + this.statusBarItem.hide(); + } + + dispose(): void { + this.statusBarItem.dispose(); + } + + constructor(private statusBarItem: vscode.StatusBarItem) { + } +} \ No newline at end of file diff --git a/src/textEditorAdapter.ts b/src/textEditorAdapter.ts new file mode 100644 index 0000000000..c08b196c51 --- /dev/null +++ b/src/textEditorAdapter.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscodeAdapter from './vscodeAdapter'; +import * as vscode from 'vscode'; + +export class TextEditorAdapter implements vscodeAdapter.TextEditor { + + get document(): any { + return this.textEditor.document; + } + + constructor(private textEditor: vscode.TextEditor) { + } +} \ No newline at end of file diff --git a/src/vscodeAdapter.ts b/src/vscodeAdapter.ts index f989ba7980..7ab5d97bbf 100644 --- a/src/vscodeAdapter.ts +++ b/src/vscodeAdapter.ts @@ -79,3 +79,768 @@ export enum ViewColumn { */ Three = 3 } + +export interface WorkspaceConfiguration { + + /** + * Return a value from this configuration. + * + * @param section Configuration name, supports _dotted_ names. + * @return The value `section` denotes or `undefined`. + */ + get(section: string): T | undefined; + + /** + * Return a value from this configuration. + * + * @param section Configuration name, supports _dotted_ names. + * @param defaultValue A value should be returned when no value could be found, is `undefined`. + * @return The value `section` denotes or the default. + */ + get(section: string, defaultValue: T): T; + + /** + * Check if this configuration has a certain value. + * + * @param section Configuration name, supports _dotted_ names. + * @return `true` if the section doesn't resolve to `undefined`. + */ + has(section: string): boolean; + + /** + * Retrieve all information about a configuration setting. A configuration value + * often consists of a *default* value, a global or installation-wide value, + * a workspace-specific value and a folder-specific value. + * + * The *effective* value (returned by [`get`](#WorkspaceConfiguration.get)) + * is computed like this: `defaultValue` overwritten by `globalValue`, + * `globalValue` overwritten by `workspaceValue`. `workspaceValue` overwritten by `workspaceFolderValue`. + * Refer to [Settings Inheritence](https://code.visualstudio.com/docs/getstarted/settings) + * for more information. + * + * *Note:* The configuration name must denote a leaf in the configuration tree + * (`editor.fontSize` vs `editor`) otherwise no result is returned. + * + * @param section Configuration name, supports _dotted_ names. + * @return Information about a configuration setting or `undefined`. + */ + inspect(section: string): { key: string; defaultValue?: T; globalValue?: T; workspaceValue?: T, workspaceFolderValue?: T } | undefined; + + /** + * Update a configuration value. The updated configuration values are persisted. + * + * A value can be changed in + * + * - [Global configuration](#ConfigurationTarget.Global): Changes the value for all instances of the editor. + * - [Workspace configuration](#ConfigurationTarget.Workspace): Changes the value for current workspace, if available. + * - [Workspace folder configuration](#ConfigurationTarget.WorkspaceFolder): Changes the value for the + * [Workspace folder](#workspace.workspaceFolders) to which the current [configuration](#WorkspaceConfiguration) is scoped to. + * + * *Note 1:* Setting a global value in the presence of a more specific workspace value + * has no observable effect in that workspace, but in others. Setting a workspace value + * in the presence of a more specific folder value has no observable effect for the resources + * under respective [folder](#workspace.workspaceFolders), but in others. Refer to + * [Settings Inheritence](https://code.visualstudio.com/docs/getstarted/settings) for more information. + * + * *Note 2:* To remove a configuration value use `undefined`, like so: `config.update('somekey', undefined)` + * + * Will throw error when + * - Writing a configuration which is not registered. + * - Writing a configuration to workspace or folder target when no workspace is opened + * - Writing a configuration to folder target when there is no folder settings + * - Writing to folder target without passing a resource when getting the configuration (`workspace.getConfiguration(section, resource)`) + * - Writing a window configuration to folder target + * + * @param section Configuration name, supports _dotted_ names. + * @param value The new value. + * @param configurationTarget The [configuration target](#ConfigurationTarget) or a boolean value. + * - If `true` configuration target is `ConfigurationTarget.Global`. + * - If `false` configuration target is `ConfigurationTarget.Workspace`. + * - If `undefined` or `null` configuration target is + * `ConfigurationTarget.WorkspaceFolder` when configuration is resource specific + * `ConfigurationTarget.Workspace` otherwise. + */ + update(section: string, value: any, configurationTarget?: ConfigurationTarget | boolean): Thenable; + + /** + * Readable dictionary that backs this configuration. + */ + readonly [key: string]: any; +} + +/** + * The configuration target + */ +export enum ConfigurationTarget { + /** + * Global configuration + */ + Global = 1, + + /** + * Workspace configuration + */ + Workspace = 2, + + /** + * Workspace folder configuration + */ + WorkspaceFolder = 3 +} + +/** + * Represents the alignment of status bar items. + */ +export enum StatusBarAlignment { + + /** + * Aligned to the left side. + */ + Left = 1, + + /** + * Aligned to the right side. + */ + Right = 2 +} + + +export interface StatusBarItem { + + /** + * The alignment of this item. + */ + readonly alignment: StatusBarAlignment; + + /** + * The priority of this item. Higher value means the item should + * be shown more to the left. + */ + readonly priority: number; + + /** + * The text to show for the entry. You can embed icons in the text by leveraging the syntax: + * + * `My text $(icon-name) contains icons like $(icon'name) this one.` + * + * Where the icon-name is taken from the [octicon](https://octicons.github.com) icon set, e.g. + * `light-bulb`, `thumbsup`, `zap` etc. + */ + text: string; + + /** + * The tooltip text when you hover over this entry. + */ + tooltip: string | undefined; + + /** + * The foreground color for this entry. + */ + color: string | undefined; + + /** + * The identifier of a command to run on click. The command must be + * [known](#commands.getCommands). + */ + command: string | undefined; + + /** + * Shows the entry in the status bar. + */ + show(): void; + + /** + * Hide the entry in the status bar. + */ + hide(): void; + + /** + * Dispose and free associated resources. Call + * [hide](#StatusBarItem.hide). + */ + dispose(): void; +} + +export interface DocumentFilter { + + /** + * A language id, like `typescript`. + */ + language?: string; + + /** + * A Uri [scheme](#Uri.scheme), like `file` or `untitled`. + */ + scheme?: string; + + /** + * A [glob pattern](#GlobPattern) that is matched on the absolute path of the document. Use a [relative pattern](#RelativePattern) + * to filter documents to a [workspace folder](#WorkspaceFolder). + */ + pattern?: GlobPattern; +} + +export type GlobPattern = string; + +export type DocumentSelector = string | DocumentFilter | (string | DocumentFilter)[]; + +export interface MessageOptions { + + /** + * Indicates that this message should be modal. + */ + modal?: boolean; +} + +export interface TextEditor { + + /** + * The document associated with this text editor. The document will be the same for the entire lifetime of this text editor. + */ + document: TextDocument; +} + +/** + * A universal resource identifier representing either a file on disk + * or another resource, like untitled resources. + */ +export interface Uri { + + /** + * Create an URI from a file system path. The [scheme](#Uri.scheme) + * will be `file`. + * + * @param path A file system or UNC path. + * @return A new Uri instance. + */ + + /** + * Create an URI from a string. Will throw if the given value is not + * valid. + * + * @param value The string value of an Uri. + * @return A new Uri instance. + */ + /** + * Scheme is the `http` part of `http://www.msft.com/some/path?query#fragment`. + * The part before the first colon. + */ + readonly scheme: string; + + /** + * Authority is the `www.msft.com` part of `http://www.msft.com/some/path?query#fragment`. + * The part between the first double slashes and the next slash. + */ + readonly authority: string; + + /** + * Path is the `/some/path` part of `http://www.msft.com/some/path?query#fragment`. + */ + readonly path: string; + + /** + * Query is the `query` part of `http://www.msft.com/some/path?query#fragment`. + */ + readonly query: string; + + /** + * Fragment is the `fragment` part of `http://www.msft.com/some/path?query#fragment`. + */ + readonly fragment: string; + + /** + * The string representing the corresponding file system path of this Uri. + * + * Will handle UNC paths and normalize windows drive letters to lower-case. Also + * uses the platform specific path separator. Will *not* validate the path for + * invalid characters and semantics. Will *not* look at the scheme of this Uri. + */ + readonly fsPath: string; + + /** + * Derive a new Uri from this Uri. + * + * ```ts + * let file = Uri.parse('before:some/file/path'); + * let other = file.with({ scheme: 'after' }); + * assert.ok(other.toString() === 'after:some/file/path'); + * ``` + * + * @param change An object that describes a change to this Uri. To unset components use `null` or + * the empty string. + * @return A new Uri that reflects the given change. Will return `this` Uri if the change + * is not changing anything. + */ + with(change: { scheme?: string; authority?: string; path?: string; query?: string; fragment?: string }): Uri; + + /** + * Returns a string representation of this Uri. The representation and normalization + * of a URI depends on the scheme. The resulting string can be safely used with + * [Uri.parse](#Uri.parse). + * + * @param skipEncoding Do not percentage-encode the result, defaults to `false`. Note that + * the `#` and `?` characters occuring in the path will always be encoded. + * @returns A string representation of this Uri. + */ + toString(skipEncoding?: boolean): string; + + /** + * Returns a JSON representation of this Uri. + * + * @return An object. + */ + toJSON(): any; +} + +export interface MessageItem { + + /** + * A short title like 'Retry', 'Open Log' etc. + */ + title: string; + + /** + * Indicates that this item replaces the default + * 'Close' action. + */ + isCloseAffordance?: boolean; +} + +/** + * Represents a text document, such as a source file. Text documents have + * [lines](#TextLine) and knowledge about an underlying resource like a file. + */ +export interface TextDocument { + + /** + * The associated URI for this document. Most documents have the __file__-scheme, indicating that they + * represent files on disk. However, some documents may have other schemes indicating that they are not + * available on disk. + */ + readonly uri: Uri; + + /** + * The file system path of the associated resource. Shorthand + * notation for [TextDocument.uri.fsPath](#TextDocument.uri). Independent of the uri scheme. + */ + readonly fileName: string; + + /** + * Is this document representing an untitled file. + */ + readonly isUntitled: boolean; + + /** + * The identifier of the language associated with this document. + */ + readonly languageId: string; + + /** + * The version number of this document (it will strictly increase after each + * change, including undo/redo). + */ + readonly version: number; + + /** + * `true` if there are unpersisted changes. + */ + readonly isDirty: boolean; + + /** + * `true` if the document have been closed. A closed document isn't synchronized anymore + * and won't be re-used when the same resource is opened again. + */ + readonly isClosed: boolean; + + /** + * Save the underlying file. + * + * @return A promise that will resolve to true when the file + * has been saved. If the file was not dirty or the save failed, + * will return false. + */ + save(): Thenable; + + /** + * The [end of line](#EndOfLine) sequence that is predominately + * used in this document. + */ + readonly eol: EndOfLine; + + /** + * The number of lines in this document. + */ + readonly lineCount: number; + + /** + * Returns a text line denoted by the line number. Note + * that the returned object is *not* live and changes to the + * document are not reflected. + * + * @param line A line number in [0, lineCount). + * @return A [line](#TextLine). + */ + lineAt(line: number): TextLine; + + /** + * Returns a text line denoted by the position. Note + * that the returned object is *not* live and changes to the + * document are not reflected. + * + * The position will be [adjusted](#TextDocument.validatePosition). + * + * @see [TextDocument.lineAt](#TextDocument.lineAt) + * @param position A position. + * @return A [line](#TextLine). + */ + lineAt(position: Position): TextLine; + + /** + * Converts the position to a zero-based offset. + * + * The position will be [adjusted](#TextDocument.validatePosition). + * + * @param position A position. + * @return A valid zero-based offset. + */ + offsetAt(position: Position): number; + + /** + * Converts a zero-based offset to a position. + * + * @param offset A zero-based offset. + * @return A valid [position](#Position). + */ + positionAt(offset: number): Position; + + /** + * Get the text of this document. A substring can be retrieved by providing + * a range. The range will be [adjusted](#TextDocument.validateRange). + * + * @param range Include only the text included by the range. + * @return The text inside the provided range or the entire text. + */ + getText(range?: Range): string; + + /** + * Get a word-range at the given position. By default words are defined by + * common separators, like space, -, _, etc. In addition, per languge custom + * [word definitions](#LanguageConfiguration.wordPattern) can be defined. It + * is also possible to provide a custom regular expression. + * + * * *Note 1:* A custom regular expression must not match the empty string and + * if it does, it will be ignored. + * * *Note 2:* A custom regular expression will fail to match multiline strings + * and in the name of speed regular expressions should not match words with + * spaces. Use [`TextLine.text`](#TextLine.text) for more complex, non-wordy, scenarios. + * + * The position will be [adjusted](#TextDocument.validatePosition). + * + * @param position A position. + * @param regex Optional regular expression that describes what a word is. + * @return A range spanning a word, or `undefined`. + */ + getWordRangeAtPosition(position: Position, regex?: RegExp): Range | undefined; + + /** + * Ensure a range is completely contained in this document. + * + * @param range A range. + * @return The given range or a new, adjusted range. + */ + validateRange(range: Range): Range; + + /** + * Ensure a position is contained in the range of this document. + * + * @param position A position. + * @return The given position or a new, adjusted position. + */ + validatePosition(position: Position): Position; +} + +/** + * Represents an end of line character sequence in a [document](#TextDocument). + */ +export enum EndOfLine { + /** + * The line feed `\n` character. + */ + LF = 1, + /** + * The carriage return line feed `\r\n` sequence. + */ + CRLF = 2 +} + +/** + * Represents a line and character position, such as + * the position of the cursor. + * + * Position objects are __immutable__. Use the [with](#Position.with) or + * [translate](#Position.translate) methods to derive new positions + * from an existing position. + */ +export interface Position { + + /** + * The zero-based line value. + */ + readonly line: number; + + /** + * The zero-based character value. + */ + readonly character: number; + + /** + * @param line A zero-based line value. + * @param character A zero-based character value. + */ + + /** + * Check if `other` is before this position. + * + * @param other A position. + * @return `true` if position is on a smaller line + * or on the same line on a smaller character. + */ + isBefore(other: Position): boolean; + + /** + * Check if `other` is before or equal to this position. + * + * @param other A position. + * @return `true` if position is on a smaller line + * or on the same line on a smaller or equal character. + */ + isBeforeOrEqual(other: Position): boolean; + + /** + * Check if `other` is after this position. + * + * @param other A position. + * @return `true` if position is on a greater line + * or on the same line on a greater character. + */ + isAfter(other: Position): boolean; + + /** + * Check if `other` is after or equal to this position. + * + * @param other A position. + * @return `true` if position is on a greater line + * or on the same line on a greater or equal character. + */ + isAfterOrEqual(other: Position): boolean; + + /** + * Check if `other` equals this position. + * + * @param other A position. + * @return `true` if the line and character of the given position are equal to + * the line and character of this position. + */ + isEqual(other: Position): boolean; + + /** + * Compare this to `other`. + * + * @param other A position. + * @return A number smaller than zero if this position is before the given position, + * a number greater than zero if this position is after the given position, or zero when + * this and the given position are equal. + */ + compareTo(other: Position): number; + + /** + * Create a new position relative to this position. + * + * @param lineDelta Delta value for the line value, default is `0`. + * @param characterDelta Delta value for the character value, default is `0`. + * @return A position which line and character is the sum of the current line and + * character and the corresponding deltas. + */ + translate(lineDelta?: number, characterDelta?: number): Position; + + /** + * Derived a new position relative to this position. + * + * @param change An object that describes a delta to this position. + * @return A position that reflects the given delta. Will return `this` position if the change + * is not changing anything. + */ + translate(change: { lineDelta?: number; characterDelta?: number; }): Position; + + /** + * Create a new position derived from this position. + * + * @param line Value that should be used as line value, default is the [existing value](#Position.line) + * @param character Value that should be used as character value, default is the [existing value](#Position.character) + * @return A position where line and character are replaced by the given values. + */ + with(line?: number, character?: number): Position; + + /** + * Derived a new position from this position. + * + * @param change An object that describes a change to this position. + * @return A position that reflects the given change. Will return `this` position if the change + * is not changing anything. + */ + with(change: { line?: number; character?: number; }): Position; +} + +export interface Range { + + /** + * The start position. It is before or equal to [end](#Range.end). + */ + readonly start: Position; + + /** + * The end position. It is after or equal to [start](#Range.start). + */ + readonly end: Position; + + /** + * `true` if `start` and `end` are equal. + */ + isEmpty: boolean; + + /** + * `true` if `start.line` and `end.line` are equal. + */ + isSingleLine: boolean; + + /** + * Check if a position or a range is contained in this range. + * + * @param positionOrRange A position or a range. + * @return `true` if the position or range is inside or equal + * to this range. + */ + contains(positionOrRange: Position | Range): boolean; + + /** + * Check if `other` equals this range. + * + * @param other A range. + * @return `true` when start and end are [equal](#Position.isEqual) to + * start and end of this range. + */ + isEqual(other: Range): boolean; + + /** + * Intersect `range` with this range and returns a new range or `undefined` + * if the ranges have no overlap. + * + * @param range A range. + * @return A range of the greater start and smaller end positions. Will + * return undefined when there is no overlap. + */ + intersection(range: Range): Range | undefined; + + /** + * Compute the union of `other` with this range. + * + * @param other A range. + * @return A range of smaller start position and the greater end position. + */ + union(other: Range): Range; + + /** + * Derived a new range from this range. + * + * @param start A position that should be used as start. The default value is the [current start](#Range.start). + * @param end A position that should be used as end. The default value is the [current end](#Range.end). + * @return A range derived from this range with the given start and end position. + * If start and end are not different `this` range will be returned. + */ + with(start?: Position, end?: Position): Range; + + /** + * Derived a new range from this range. + * + * @param change An object that describes a change to this range. + * @return A range that reflects the given change. Will return `this` range if the change + * is not changing anything. + */ + with(change: { start?: Position, end?: Position }): Range; +} + +/** + * Represents a line of text, such as a line of source code. + * + * TextLine objects are __immutable__. When a [document](#TextDocument) changes, + * previously retrieved lines will not represent the latest state. + */ +export interface TextLine { + + /** + * The zero-based line number. + */ + readonly lineNumber: number; + + /** + * The text of this line without the line separator characters. + */ + readonly text: string; + + /** + * The range this line covers without the line separator characters. + */ + readonly range: Range; + + /** + * The range this line covers with the line separator characters. + */ + readonly rangeIncludingLineBreak: Range; + + /** + * The offset of the first character which is not a whitespace character as defined + * by `/\s/`. **Note** that if a line is all whitespaces the length of the line is returned. + */ + readonly firstNonWhitespaceCharacterIndex: number; + + /** + * Whether this line is whitespace only, shorthand + * for [TextLine.firstNonWhitespaceCharacterIndex](#TextLine.firstNonWhitespaceCharacterIndex) === [TextLine.text.length](#TextLine.text). + */ + readonly isEmptyOrWhitespace: boolean; +} + +/** + * Thenable is a common denominator between ES6 promises, Q, jquery.Deferred, WinJS.Promise, + * and others. This API makes no assumption about what promise libary is being used which + * enables reusing existing code without migrating to a specific promise implementation. Still, + * we recommend the use of native promises which are available in this editor. + */ +interface Thenable { + /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of which ever callback is executed. + */ + then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => TResult | Thenable): Thenable; + then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => void): Thenable; +} + +export interface vscode { + commands: { + executeCommand: (command: string, ...rest: any[]) => Thenable; + }; + languages: { + match: (selector: DocumentSelector, document: TextDocument) => number; + }; + window: { + activeTextEditor: TextEditor | undefined; + showInformationMessage: (message: string, ...items: string[]) => Thenable; + showWarningMessage: (message: string, ...items: T[]) => Thenable; + }; + workspace: { + getConfiguration: (section?: string, resource?: Uri) => WorkspaceConfiguration; + asRelativePath: (pathOrUri: string | Uri, includeWorkspaceFolder?: boolean) => string; + }; +} \ No newline at end of file diff --git a/test/unitTests/logging/Fakes.ts b/test/unitTests/logging/Fakes.ts index 7bc15add53..cba10ed42a 100644 --- a/test/unitTests/logging/Fakes.ts +++ b/test/unitTests/logging/Fakes.ts @@ -4,7 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from '../../../src/vscodeAdapter'; +import * as protocol from '../../../src/omnisharp/protocol'; +import { DocumentSelector, MessageItem, TextDocument, Uri } from '../../../src/vscodeAdapter'; import { ITelemetryReporter } from '../../../src/observers/TelemetryObserver'; +import { MSBuildDiagnosticsMessage } from '../../../src/omnisharp/protocol'; +import { OmnisharpServerMsBuildProjectDiagnostics, OmnisharpServerOnError, OmnisharpServerUnresolvedDependencies, WorkspaceInformationUpdated } from '../../../src/omnisharp/loggingEvents'; export const getNullChannel = (): vscode.OutputChannel => { let returnChannel: vscode.OutputChannel = { @@ -25,4 +29,109 @@ export const getNullTelemetryReporter = (): ITelemetryReporter => { }; return reporter; -}; \ No newline at end of file +}; + +export const getNullWorkspaceConfiguration = (): vscode.WorkspaceConfiguration => { + let workspace: vscode.WorkspaceConfiguration = { + get: (section: string) => { + return true; + }, + has: (section: string) => { return true; }, + inspect: () => { + return { + key: "somekey" + }; + }, + update: () => { return Promise.resolve(); }, + }; + return workspace; +}; + +export function getOmnisharpMSBuildProjectDiagnosticsEvent(fileName: string, warnings: MSBuildDiagnosticsMessage[], errors: MSBuildDiagnosticsMessage[]): OmnisharpServerMsBuildProjectDiagnostics { + return new OmnisharpServerMsBuildProjectDiagnostics({ + FileName: fileName, + Warnings: warnings, + Errors: errors + }); +} + +export function getMSBuildDiagnosticsMessage(logLevel: string, + fileName: string, + text: string, + startLine: number, + startColumn: number, + endLine: number, + endColumn: number): MSBuildDiagnosticsMessage { + return { + LogLevel: logLevel, + FileName: fileName, + Text: text, + StartLine: startLine, + StartColumn: startColumn, + EndLine: endLine, + EndColumn: endColumn + }; +} + +export function getOmnisharpServerOnErrorEvent(text: string, fileName: string, line: number, column: number): OmnisharpServerOnError { + return new OmnisharpServerOnError({ + Text: text, + FileName: fileName, + Line: line, + Column: column + }); +} + +export function getUnresolvedDependenices(fileName: string): OmnisharpServerUnresolvedDependencies { + return new OmnisharpServerUnresolvedDependencies({ + UnresolvedDependencies: [], + FileName: fileName + }); +} + +export function getFakeVsCode(): vscode.vscode { + return { + commands: { + executeCommand: (command: string, ...rest: any[]) => { + throw new Error("Not Implemented"); + } + }, + languages: { + match: (selector: DocumentSelector, document: TextDocument) => { + throw new Error("Not Implemented"); + } + }, + window: { + activeTextEditor: undefined, + showInformationMessage: (message: string, ...items: string[]) => { + throw new Error("Not Implemented"); + }, + showWarningMessage: (message: string, ...items: T[]) => { + throw new Error("Not Implemented"); + } + }, + workspace: { + getConfiguration: (section?: string, resource?: Uri) => { + throw new Error("Not Implemented"); + }, + asRelativePath: (pathOrUri: string | Uri, includeWorkspaceFolder?: boolean) => { + throw new Error("Not Implemented"); + } + } + }; +} + +export function getMSBuildWorkspaceInformation(msBuildSolutionPath: string, msBuildProjects: protocol.MSBuildProject[]): protocol.MsBuildWorkspaceInformation { + return { + SolutionPath: msBuildSolutionPath, + Projects: msBuildProjects + }; +} + +export function getWorkspaceInformationUpdated(msbuild: protocol.MsBuildWorkspaceInformation): WorkspaceInformationUpdated { + let a: protocol.WorkspaceInformationResponse = { + MsBuild: msbuild + }; + + return new WorkspaceInformationUpdated(a); +} \ No newline at end of file diff --git a/test/unitTests/logging/InformationMessageObserver.test.ts b/test/unitTests/logging/InformationMessageObserver.test.ts new file mode 100644 index 0000000000..a80eefed44 --- /dev/null +++ b/test/unitTests/logging/InformationMessageObserver.test.ts @@ -0,0 +1,120 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as rx from 'rx'; +import { InformationMessageObserver } from '../../../src/observers/InformationMessageObserver'; +import { use as chaiUse, expect, should } from 'chai'; +import { vscode, Uri } from '../../../src/vscodeAdapter'; +import { getFakeVsCode, getNullWorkspaceConfiguration, getUnresolvedDependenices } from './Fakes'; + +chaiUse(require('chai-as-promised')); +chaiUse(require('chai-string')); + +suite("InformationMessageObserver", () => { + suiteSetup(() => should()); + + let doClickOk: () => void; + let doClickCancel: () => void; + let signalCommandDone: () => void; + let commandDone = new Promise(resolve => { + signalCommandDone = () => { resolve(); }; + }); + let vscode: vscode = getFakeVsCode(); + let infoMessage; + let relativePath; + let invokedCommand; + let observer: InformationMessageObserver = new InformationMessageObserver(vscode); + + vscode.window.showInformationMessage = (message: string, ...items: string[]) => { + infoMessage = message; + return new Promise(resolve => { + doClickCancel = () => { + resolve(undefined); + }; + + doClickOk = () => { + resolve(message); + }; + }); + }; + + vscode.commands.executeCommand = (command: string, ...rest: any[]) => { + invokedCommand = command; + signalCommandDone(); + return undefined; + }; + + vscode.workspace.asRelativePath = (pathOrUri?: string | Uri, includeWorspaceFolder?: boolean) => { + relativePath = pathOrUri; + return relativePath; + }; + + setup(() => { + infoMessage = undefined; + relativePath = undefined; + invokedCommand = undefined; + commandDone = new Promise(resolve => { + signalCommandDone = () => { resolve(); }; + }); + }); + + suite('OmnisharpServerUnresolvedDependencies', () => { + let event = getUnresolvedDependenices("someFile"); + + suite('Suppress Dotnet Restore Notification is true', () => { + setup(() => { + vscode.workspace.getConfiguration = (section?: string, resource?: Uri) => { + return { + ...getNullWorkspaceConfiguration(), + get: (section: string) => { + return true;// suppress the restore information + } + }; + }; + }); + + test('The information message is not shown', () => { + observer.post(event); + expect(infoMessage).to.be.undefined; + }); + }); + + suite('Suppress Dotnet Restore Notification is false', () => { + setup(() => { + vscode.workspace.getConfiguration = (section?: string, resource?: Uri) => { + return { + ...getNullWorkspaceConfiguration(), + get: (section: string) => { + return false; // do not suppress the restore info + } + }; + }; + }); + + test('The information message is shown', async () => { + observer.post(event); + expect(relativePath).to.not.be.empty; + expect(infoMessage).to.not.be.empty; + doClickOk(); + await commandDone; + expect(invokedCommand).to.be.equal('dotnet.restore'); + }); + + test('Given an information message if the user clicks Restore, the command is executed', async () => { + observer.post(event); + doClickOk(); + await commandDone; + expect(invokedCommand).to.be.equal('dotnet.restore'); + }); + + test('Given an information message if the user clicks cancel, the command is not executed', async () => { + observer.post(event); + doClickCancel(); + await expect(rx.Observable.fromPromise(commandDone).timeout(1).toPromise()).to.be.rejected; + expect(invokedCommand).to.be.undefined; + }); + }); + }); +}); \ No newline at end of file diff --git a/test/unitTests/logging/OmnisharpLoggerObserver.test.ts b/test/unitTests/logging/OmnisharpLoggerObserver.test.ts index 293d840bcb..d906ed21f0 100644 --- a/test/unitTests/logging/OmnisharpLoggerObserver.test.ts +++ b/test/unitTests/logging/OmnisharpLoggerObserver.test.ts @@ -75,7 +75,6 @@ suite("OmnisharpLoggerObserver", () => { [ new OmnisharpServerOnStdErr("on std error message"), new OmnisharpServerMessage("server message"), - new OmnisharpServerOnServerError("on server error message"), ].forEach((event: EventWithMessage) => { test(`${event.constructor.name}: Message is logged`, () => { observer.post(event); @@ -83,6 +82,13 @@ suite("OmnisharpLoggerObserver", () => { }); }); + test(`OmnisharpServerOnServerError: Message is logged`, () => { + let event = new OmnisharpServerOnServerError("on server error message"); + observer.post(event); + expect(logOutput).to.contain(event.err); + }); + + [ new OmnisharpInitialisation(new Date(5), "somePath"), ].forEach((event: OmnisharpInitialisation) => { diff --git a/test/unitTests/logging/OmnisharpStatusBarObserver.test.ts b/test/unitTests/logging/OmnisharpStatusBarObserver.test.ts new file mode 100644 index 0000000000..35b56199af --- /dev/null +++ b/test/unitTests/logging/OmnisharpStatusBarObserver.test.ts @@ -0,0 +1,71 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DocumentSelector, StatusBarItem } from '../../../src/vscodeAdapter'; +import { OmnisharpOnBeforeServerInstall, OmnisharpOnBeforeServerStart, OmnisharpOnMultipleLaunchTargets, OmnisharpServerOnServerError, OmnisharpServerOnStart, OmnisharpServerOnStop } from '../../../src/omnisharp/loggingEvents'; +import { expect, should } from 'chai'; +import { OmnisharpStatusBarObserver } from '../../../src/observers/OmnisharpStatusBarObserver'; +import { getFakeVsCode, getWorkspaceInformationUpdated, getMSBuildWorkspaceInformation } from './Fakes'; + +suite('OmnisharpStatusBarObserver', () => { + suiteSetup(() => should()); + let output = ''; + let showCalled: boolean; + let hideCalled: boolean; + + setup(() => { + output = ''; + showCalled = false; + hideCalled = false; + }); + + let statusBarItem = { + show: () => { showCalled = true; }, + hide: () => { hideCalled = true; } + }; + + let observer = new OmnisharpStatusBarObserver( statusBarItem); + + test('OnServerError: Status bar is shown with the error text', () => { + let event = new OmnisharpServerOnServerError("someError"); + observer.post(event); + expect(showCalled).to.be.true; + expect(statusBarItem.text).to.equal(`$(flame) Error starting OmniSharp`); + expect(statusBarItem.command).to.equal('o.showOutput'); + }); + + test('OnBeforeServerInstall: Status bar is shown with the installation text', () => { + let event = new OmnisharpOnBeforeServerInstall(); + observer.post(event); + expect(showCalled).to.be.true; + expect(statusBarItem.text).to.be.equal('$(flame) Installing OmniSharp...'); + expect(statusBarItem.command).to.equal('o.showOutput'); + }); + + test('OnBeforeServerStart: Status bar is shown with the starting text', () => { + let event = new OmnisharpOnBeforeServerStart(); + observer.post(event); + expect(showCalled).to.be.true; + expect(statusBarItem.text).to.be.equal('$(flame) Starting...'); + expect(statusBarItem.command).to.equal('o.showOutput'); + }); + + test('OnServerStart: Status bar is shown with the flame and "Running" text', () => { + let event = new OmnisharpServerOnStart(); + observer.post(event); + expect(showCalled).to.be.true; + expect(statusBarItem.text).to.be.equal('$(flame) Running'); + expect(statusBarItem.command).to.equal('o.showOutput'); + }); + + test('OnServerStop: Status bar is hidden and the attributes are set to undefined', () => { + let event = new OmnisharpServerOnStop(); + observer.post(event); + expect(hideCalled).to.be.true; + expect(statusBarItem.text).to.be.undefined; + expect(statusBarItem.command).to.be.undefined; + expect(statusBarItem.color).to.be.undefined; + }); +}); \ No newline at end of file diff --git a/test/unitTests/logging/ProjectStatusBarObserver.test.ts b/test/unitTests/logging/ProjectStatusBarObserver.test.ts new file mode 100644 index 0000000000..7b9ba51473 --- /dev/null +++ b/test/unitTests/logging/ProjectStatusBarObserver.test.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { expect, should } from 'chai'; +import { getFakeVsCode, getWorkspaceInformationUpdated, getMSBuildWorkspaceInformation } from './Fakes'; +import { vscode, StatusBarItem } from '../../../src/vscodeAdapter'; +import { ProjectStatusBarObserver } from '../../../src/observers/ProjectStatusBarObserver'; +import { OmnisharpOnMultipleLaunchTargets, OmnisharpServerOnStop } from '../../../src/omnisharp/loggingEvents'; + +suite('ProjectStatusBarObserver', () => { + suiteSetup(() => should()); + + let output = ''; + let showCalled: boolean; + let hideCalled: boolean; + let statusBarItem = { + show: () => { showCalled = true; }, + hide: () => { hideCalled = true; } + }; + let observer = new ProjectStatusBarObserver(statusBarItem); + + setup(() => { + output = ''; + showCalled = false; + hideCalled = false; + }); + + test('OnServerStop: Status bar is hidden and the attributes are set to undefined', () => { + let event = new OmnisharpServerOnStop(); + observer.post(event); + expect(hideCalled).to.be.true; + expect(statusBarItem.text).to.be.undefined; + expect(statusBarItem.command).to.be.undefined; + expect(statusBarItem.color).to.be.undefined; + }); + + test('OnMultipleLaunchTargets: Status bar is shown with the select project option and the comand to pick a project', () => { + let event = new OmnisharpOnMultipleLaunchTargets([]); + observer.post(event); + expect(showCalled).to.be.true; + expect(statusBarItem.text).to.contain('Select project'); + expect(statusBarItem.command).to.equal('o.pickProjectAndStart'); + }); + + suite('WorkspaceInformationUpdated', () => { + test('Project status is hidden if there is no MSBuild Object', () => { + let event = getWorkspaceInformationUpdated(null); + observer.post(event); + expect(hideCalled).to.be.true; + expect(statusBarItem.text).to.be.undefined; + expect(statusBarItem.command).to.be.undefined; + }); + + test('Project status is shown if there is an MSBuild object', () => { + let event = getWorkspaceInformationUpdated(getMSBuildWorkspaceInformation("somePath", [])); + observer.post(event); + expect(showCalled).to.be.true; + expect(statusBarItem.text).to.contain(event.info.MsBuild.SolutionPath); + expect(statusBarItem.command).to.equal('o.pickProjectAndStart'); + }); + }); +}); \ No newline at end of file diff --git a/test/unitTests/logging/WarningMessageObserver.test.ts b/test/unitTests/logging/WarningMessageObserver.test.ts new file mode 100644 index 0000000000..203c3ea54a --- /dev/null +++ b/test/unitTests/logging/WarningMessageObserver.test.ts @@ -0,0 +1,115 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as rx from 'rx'; +import { MessageItemWithCommand, WarningMessageObserver } from '../../../src/observers/WarningMessageObserver'; +import { use as chaiUse, expect, should } from 'chai'; +import { getFakeVsCode, getMSBuildDiagnosticsMessage, getOmnisharpMSBuildProjectDiagnosticsEvent, getOmnisharpServerOnErrorEvent } from './Fakes'; +import { BaseEvent } from '../../../src/omnisharp/loggingEvents'; +import { vscode } from '../../../src/vscodeAdapter'; +import { Observable } from 'rx'; + +chaiUse(require('chai-as-promised')); +chaiUse(require('chai-string')); + +suite('WarningMessageObserver', () => { + suiteSetup(() => should()); + + let doClickOk: () => void; + let doClickCancel: () => void; + let signalCommandDone: () => void; + let commandDone = new Promise(resolve => { + signalCommandDone = () => { resolve(); }; + }); + + let warningMessage; + let invokedCommand; + let scheduler: rx.HistoricalScheduler; + let observer: WarningMessageObserver; + let vscode: vscode = getFakeVsCode(); + + vscode.window.showWarningMessage = (message, ...items) => { + warningMessage = message; + + return new Promise(resolve => { + doClickCancel = () => { + resolve(undefined); + }; + + doClickOk = () => { + resolve(items[0]); + }; + }); + }; + + vscode.commands.executeCommand = (command, ...rest) => { + invokedCommand = command; + signalCommandDone(); + return undefined; + }; + + setup(() => { + scheduler = new rx.HistoricalScheduler(0, (x, y) => { + return x > y ? 1 : -1; + }); + observer = new WarningMessageObserver(vscode, scheduler); + warningMessage = undefined; + invokedCommand = undefined; + commandDone = new Promise(resolve => { + signalCommandDone = () => { resolve(); }; + }); + }); + + test('OmnisharpServerMsBuildProjectDiagnostics: No action is taken if the errors array is empty', () => { + let event = getOmnisharpMSBuildProjectDiagnosticsEvent("someFile", + [getMSBuildDiagnosticsMessage("warningFile", "", "", 0, 0, 0, 0)], + []); + observer.post(event); + expect(invokedCommand).to.be.undefined; + }); + + [ + getOmnisharpMSBuildProjectDiagnosticsEvent("someFile", + [getMSBuildDiagnosticsMessage("warningFile", "", "", 1, 2, 3, 4)], + [getMSBuildDiagnosticsMessage("errorFile", "", "", 5, 6, 7, 8)]), + getOmnisharpServerOnErrorEvent("someText", "someFile", 1, 2) + ].forEach((event: BaseEvent) => { + suite(`${event.constructor.name}`, () => { + test(`When the event is fired then a warning message is displayed`, () => { + observer.post(event); + scheduler.advanceBy(1500); //since the debounce time is 1500 no output should be there + expect(warningMessage).to.be.equal("Some projects have trouble loading. Please review the output for more details."); + }); + + test(`When events are fired rapidly, then they are debounced by 1500 ms`, () => { + observer.post(event); + scheduler.advanceBy(1000); + expect(warningMessage).to.be.undefined; + observer.post(event); + scheduler.advanceBy(500); + expect(warningMessage).to.be.undefined; + scheduler.advanceBy(1000); + expect(warningMessage).to.not.be.empty; + //once there is a silence for 1500 ms the function will be invoked + }); + + test(`Given a warning message, when the user clicks ok the command is executed`, async () => { + observer.post(event); + scheduler.advanceBy(1500); + doClickOk(); + await commandDone; + expect(invokedCommand).to.be.equal("o.showOutput"); + }); + + test(`Given a warning message, when the user clicks cancel the command is not executed`, async () => { + observer.post(event); + scheduler.advanceBy(1500); + doClickCancel(); + await expect(Observable.fromPromise(commandDone).timeout(1).toPromise()).to.be.rejected; + expect(invokedCommand).to.be.undefined; + }); + }); + }); +}); \ No newline at end of file