diff --git a/.vscode/settings.json b/.vscode/settings.json index bf86be1b4..f32fb78fb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,5 +18,6 @@ "tslint.rulesDirectory": "node_modules/tslint-microsoft-contrib", "typescript.tsdk": "./node_modules/typescript/lib", - "mocha.enabled": true + "mocha.enabled": true, + "omnisharp.autoStart": false } \ No newline at end of file diff --git a/package.json b/package.json index bc497a5e1..ed0999423 100644 --- a/package.json +++ b/package.json @@ -799,6 +799,16 @@ "title": "Select Project", "category": "OmniSharp" }, + { + "command": "o.reanalyze.allProjects", + "title": "Analyze all projects", + "category": "OmniSharp" + }, + { + "command": "o.reanalyze.currentProject", + "title": "Analyze current project", + "category": "OmniSharp" + }, { "command": "dotnet.generateAssets", "title": "Generate Assets for Build and Debug", diff --git a/src/features/commands.ts b/src/features/commands.ts index 582df4ab5..d7b5b714d 100644 --- a/src/features/commands.ts +++ b/src/features/commands.ts @@ -31,6 +31,9 @@ export default function registerCommands(server: OmniSharpServer, platformInfo: disposable.add(vscode.commands.registerCommand('dotnet.restore.project', async () => pickProjectAndDotnetRestore(server, eventStream))); disposable.add(vscode.commands.registerCommand('dotnet.restore.all', async () => dotnetRestoreAllProjects(server, eventStream))); + disposable.add(vscode.commands.registerCommand('o.reanalyze.allProjects', async () => reAnalyzeAllProjects(server, eventStream))); + disposable.add(vscode.commands.registerCommand('o.reanalyze.currentProject', async () => reAnalyzeCurrentProject(server, eventStream))); + // register empty handler for csharp.installDebugger // running the command activates the extension, which is all we need for installation to kickoff disposable.add(vscode.commands.registerCommand('csharp.downloadDebugger', () => { })); @@ -129,6 +132,16 @@ async function pickProjectAndDotnetRestore(server: OmniSharpServer, eventStream: } } +async function reAnalyzeAllProjects(server: OmniSharpServer, eventStream: EventStream): Promise { + await serverUtils.reAnalyze(server, {}); +} + +async function reAnalyzeCurrentProject(server: OmniSharpServer, eventStream: EventStream): Promise { + await serverUtils.reAnalyze(server, { + fileName: vscode.window.activeTextEditor.document.uri.fsPath + }); +} + async function dotnetRestoreAllProjects(server: OmniSharpServer, eventStream: EventStream): Promise { let descriptors = await getProjectDescriptors(server); eventStream.post(new CommandDotNetRestoreStart()); diff --git a/src/features/diagnosticsProvider.ts b/src/features/diagnosticsProvider.ts index bdeefe9bb..b99207c61 100644 --- a/src/features/diagnosticsProvider.ts +++ b/src/features/diagnosticsProvider.ts @@ -14,6 +14,9 @@ import { IDisposable } from '../Disposable'; import { isVirtualCSharpDocument } from './virtualDocumentTracker'; import { TextDocument } from '../vscodeAdapter'; import OptionProvider from '../observers/OptionProvider'; +import { Subject, Subscription } from 'rxjs'; +import { throttleTime } from 'rxjs/operators'; +import { DiagnosticStatus } from '../omnisharp/protocol'; import { LanguageMiddlewareFeature } from '../omnisharp/LanguageMiddlewareFeature'; export class Advisor { @@ -43,7 +46,7 @@ export class Advisor { && !this._isRestoringPackages(); } - public shouldValidateProject(): boolean { + public shouldValidateAll(): boolean { return this._isServerStarted() && !this._isRestoringPackages() && !this._isOverFileLimit(); @@ -125,47 +128,55 @@ class DiagnosticsProvider extends AbstractSupport { private _validationAdvisor: Advisor; private _disposable: CompositeDisposable; - private _documentValidations: { [uri: string]: vscode.CancellationTokenSource } = Object.create(null); - private _projectValidation: vscode.CancellationTokenSource; private _diagnostics: vscode.DiagnosticCollection; + private _validateCurrentDocumentPipe = new Subject(); + private _validateAllPipe = new Subject(); + private _analyzersEnabled: boolean; + private _subscriptions: Subscription[] = []; private _suppressHiddenDiagnostics: boolean; constructor(server: OmniSharpServer, validationAdvisor: Advisor, languageMiddlewareFeature: LanguageMiddlewareFeature) { super(server, languageMiddlewareFeature); + this._analyzersEnabled = vscode.workspace.getConfiguration('omnisharp').get('enableRoslynAnalyzers', false); this._validationAdvisor = validationAdvisor; this._diagnostics = vscode.languages.createDiagnosticCollection('csharp'); this._suppressHiddenDiagnostics = vscode.workspace.getConfiguration('csharp').get('suppressHiddenDiagnostics', true); - let d1 = this._server.onPackageRestore(this._validateProject, this); - let d2 = this._server.onProjectChange(this._validateProject, this); - let d4 = vscode.workspace.onDidOpenTextDocument(event => this._onDocumentAddOrChange(event), this); - let d3 = vscode.workspace.onDidChangeTextDocument(event => this._onDocumentAddOrChange(event.document), this); - let d5 = vscode.workspace.onDidCloseTextDocument(this._onDocumentRemove, this); - let d6 = vscode.window.onDidChangeActiveTextEditor(event => this._onDidChangeActiveTextEditor(event), this); - let d7 = vscode.window.onDidChangeWindowState(event => this._OnDidChangeWindowState(event), this); - this._disposable = new CompositeDisposable(this._diagnostics, d1, d2, d3, d4, d5, d6, d7); - - // Go ahead and check for diagnostics in the currently visible editors. - for (let editor of vscode.window.visibleTextEditors) { - let document = editor.document; - if (this.shouldIgnoreDocument(document)) { - continue; - } - - this._validateDocument(document); - } + this._subscriptions.push(this._validateCurrentDocumentPipe + .asObservable() + .pipe(throttleTime(750)) + .subscribe(async x => await this._validateDocument(x))); + + this._subscriptions.push(this._validateAllPipe + .asObservable() + .pipe(throttleTime(3000)) + .subscribe(async () => { + if (this._validationAdvisor.shouldValidateAll()) { + await this._validateEntireWorkspace(); + } + else if (this._validationAdvisor.shouldValidateFiles()) { + await this._validateOpenDocuments(); + } + })); + + + this._disposable = new CompositeDisposable(this._diagnostics, + this._server.onPackageRestore(() => this._validateAllPipe.next(), this), + this._server.onProjectChange(() => this._validateAllPipe.next(), this), + this._server.onProjectDiagnosticStatus(this._onProjectAnalysis, this), + vscode.workspace.onDidOpenTextDocument(event => this._onDocumentOpenOrChange(event), this), + vscode.workspace.onDidChangeTextDocument(event => this._onDocumentOpenOrChange(event.document), this), + vscode.workspace.onDidCloseTextDocument(this._onDocumentClose, this), + vscode.window.onDidChangeActiveTextEditor(event => this._onDidChangeActiveTextEditor(event), this), + vscode.window.onDidChangeWindowState(event => this._OnDidChangeWindowState(event), this,), + ); } public dispose = () => { - if (this._projectValidation) { - this._projectValidation.dispose(); - } - - for (let key in this._documentValidations) { - this._documentValidations[key].dispose(); - } - + this._validateAllPipe.complete(); + this._validateCurrentDocumentPipe.complete(); + this._subscriptions.forEach(x => x.unsubscribe()); this._disposable.dispose(); } @@ -191,52 +202,43 @@ class DiagnosticsProvider extends AbstractSupport { private _onDidChangeActiveTextEditor(textEditor: vscode.TextEditor): void { // active text editor can be undefined. if (textEditor != undefined && textEditor.document != null) { - this._onDocumentAddOrChange(textEditor.document); + this._onDocumentOpenOrChange(textEditor.document); } } - private _onDocumentAddOrChange(document: vscode.TextDocument): void { + private _onDocumentOpenOrChange(document: vscode.TextDocument): void { if (this.shouldIgnoreDocument(document)) { return; } - this._validateDocument(document); - this._validateProject(); - } + this._validateCurrentDocumentPipe.next(document); - private _onDocumentRemove(document: vscode.TextDocument): void { - let key = document.uri; - let didChange = false; - if (this._diagnostics.get(key)) { - didChange = true; - this._diagnostics.delete(key); + // This check is just small perf optimization to reduce queries + // for omnisharp with analyzers (which has event to notify about updates.) + if (!this._analyzersEnabled) { + this._validateAllPipe.next(); } + } - let keyString = key.toString(); - - if (this._documentValidations[keyString]) { - didChange = true; - this._documentValidations[keyString].cancel(); - delete this._documentValidations[keyString]; - } - if (didChange) { - this._validateProject(); + private _onProjectAnalysis(event: protocol.ProjectDiagnosticStatus) { + if (event.Status == DiagnosticStatus.Ready) { + this._validateAllPipe.next(); } } - private _validateDocument(document: vscode.TextDocument): void { - // If we've already started computing for this document, cancel that work. - let key = document.uri.toString(); - if (this._documentValidations[key]) { - this._documentValidations[key].cancel(); + private _onDocumentClose(document: vscode.TextDocument): void { + if (this._diagnostics.has(document.uri) && !this._validationAdvisor.shouldValidateAll()) { + this._diagnostics.delete(document.uri); } + } + private _validateDocument(document: vscode.TextDocument): NodeJS.Timeout { if (!this._validationAdvisor.shouldValidateFiles()) { return; } - let source = new vscode.CancellationTokenSource(); - let handle = setTimeout(async () => { + return setTimeout(async () => { + let source = new vscode.CancellationTokenSource(); try { let value = await serverUtils.codeCheck(this._server, { FileName: document.fileName }, source.token); let quickFixes = value.QuickFixes; @@ -257,10 +259,22 @@ class DiagnosticsProvider extends AbstractSupport { catch (error) { return; } - }, 750); + }, 2000); + } + + // On large workspaces (if maxProjectFileCountForDiagnosticAnalysis) is less than workspace size, + // diagnostic fallback to mode where only open documents are analyzed. + private _validateOpenDocuments(): NodeJS.Timeout { + return setTimeout(async () => { + for (let editor of vscode.window.visibleTextEditors) { + let document = editor.document; + if (this.shouldIgnoreDocument(document)) { + continue; + } - source.token.onCancellationRequested(() => clearTimeout(handle)); - this._documentValidations[key] = source; + await this._validateDocument(document); + } + }, 3000); } private _mapQuickFixesAsDiagnosticsInFile(quickFixes: protocol.QuickFix[]): { diagnostic: vscode.Diagnostic, fileName: string }[] { @@ -269,61 +283,41 @@ class DiagnosticsProvider extends AbstractSupport { .filter(diagnosticInFile => diagnosticInFile !== undefined); } - private _validateProject(): void { - // If we've already started computing for this project, cancel that work. - if (this._projectValidation) { - this._projectValidation.cancel(); - } + private _validateEntireWorkspace(): NodeJS.Timeout { + return setTimeout(async () => { + let value = await serverUtils.codeCheck(this._server, { FileName: null }, new vscode.CancellationTokenSource().token); - if (!this._validationAdvisor.shouldValidateProject()) { - return; - } + let quickFixes = value.QuickFixes + .sort((a, b) => a.FileName.localeCompare(b.FileName)); - this._projectValidation = new vscode.CancellationTokenSource(); - let handle = setTimeout(async () => { - try { - let value = await serverUtils.codeCheck(this._server, { FileName: null }, this._projectValidation.token); - - let quickFixes = value.QuickFixes - .sort((a, b) => a.FileName.localeCompare(b.FileName)); - - let entries: [vscode.Uri, vscode.Diagnostic[]][] = []; - let lastEntry: [vscode.Uri, vscode.Diagnostic[]]; - - for (let diagnosticInFile of this._mapQuickFixesAsDiagnosticsInFile(quickFixes)) { - let uri = vscode.Uri.file(diagnosticInFile.fileName); - - if (lastEntry && lastEntry[0].toString() === uri.toString()) { - lastEntry[1].push(diagnosticInFile.diagnostic); - } else { - // We're replacing all diagnostics in this file. Pushing an entry with undefined for - // the diagnostics first ensures that the previous diagnostics for this file are - // cleared. Otherwise, new entries will be merged with the old ones. - entries.push([uri, undefined]); - lastEntry = [uri, [diagnosticInFile.diagnostic]]; - entries.push(lastEntry); - } - } + let entries: [vscode.Uri, vscode.Diagnostic[]][] = []; + let lastEntry: [vscode.Uri, vscode.Diagnostic[]]; - // Clear diagnostics for files that no longer have any diagnostics. - this._diagnostics.forEach((uri, diagnostics) => { - if (!entries.find(tuple => tuple[0].toString() === uri.toString())) { - this._diagnostics.delete(uri); - } - }); + for (let diagnosticInFile of this._mapQuickFixesAsDiagnosticsInFile(quickFixes)) { + let uri = vscode.Uri.file(diagnosticInFile.fileName); - // replace all entries - this._diagnostics.set(entries); - } - catch (error) { - return; + if (lastEntry && lastEntry[0].toString() === uri.toString()) { + lastEntry[1].push(diagnosticInFile.diagnostic); + } else { + // We're replacing all diagnostics in this file. Pushing an entry with undefined for + // the diagnostics first ensures that the previous diagnostics for this file are + // cleared. Otherwise, new entries will be merged with the old ones. + entries.push([uri, undefined]); + lastEntry = [uri, [diagnosticInFile.diagnostic]]; + entries.push(lastEntry); + } } - }, 3000); - // clear timeout on cancellation - this._projectValidation.token.onCancellationRequested(() => { - clearTimeout(handle); - }); + // Clear diagnostics for files that no longer have any diagnostics. + this._diagnostics.forEach((uri) => { + if (!entries.find(tuple => tuple[0].toString() === uri.toString())) { + this._diagnostics.delete(uri); + } + }); + + // replace all entries + this._diagnostics.set(entries); + }, 3000); } private _asDiagnosticInFileIfAny(quickFix: protocol.QuickFix): { diagnostic: vscode.Diagnostic, fileName: string } { diff --git a/src/main.ts b/src/main.ts index 77e96992a..24215760c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -43,6 +43,7 @@ import { downloadAndInstallPackages } from './packageManager/downloadAndInstallP import IInstallDependencies from './packageManager/IInstallDependencies'; import { installRuntimeDependencies } from './InstallRuntimeDependencies'; import { isValidDownload } from './packageManager/isValidDownload'; +import { BackgroundWorkStatusBarObserver } from './observers/BackgroundWorkStatusBarObserver'; export async function activate(context: vscode.ExtensionContext): Promise { @@ -91,14 +92,18 @@ export async function activate(context: vscode.ExtensionContext): Promise { + if(event.type === EventType.ProjectDiagnosticStatus) + { + let asProjectEvent = event; + + if(asProjectEvent.message.Status === DiagnosticStatus.Processing) + { + let projectFile = asProjectEvent.message.ProjectFilePath.replace(/^.*[\\\/]/, ''); + this.SetAndShowStatusBar(`$(sync~spin) Analyzing ${projectFile}`, 'o.showOutput', null, `Analyzing ${projectFile}`); + } + else + { + this.ResetAndHideStatusBar(); + } + } + } +} + diff --git a/src/omnisharp/EventType.ts b/src/omnisharp/EventType.ts index 1c114fc69..db1a32965 100644 --- a/src/omnisharp/EventType.ts +++ b/src/omnisharp/EventType.ts @@ -79,6 +79,7 @@ export enum EventType { OmnisharpServerOnStart = 72, OmnisharpOnBeforeServerInstall = 73, ProjectConfigurationReceived = 74, + ProjectDiagnosticStatus = 75 } //Note that the EventType protocol is shared with Razor.VSCode and the numbers here should not be altered diff --git a/src/omnisharp/loggingEvents.ts b/src/omnisharp/loggingEvents.ts index a4f7d6ed5..63f63dde9 100644 --- a/src/omnisharp/loggingEvents.ts +++ b/src/omnisharp/loggingEvents.ts @@ -92,6 +92,11 @@ export class OmnisharpServerOnError implements BaseEvent { constructor(public errorMessage: protocol.ErrorMessage) { } } +export class OmnisharpProjectDiagnosticStatus implements BaseEvent { + type=EventType.ProjectDiagnosticStatus; + constructor(public message: protocol.ProjectDiagnosticStatus) { } +} + export class OmnisharpServerMsBuildProjectDiagnostics implements BaseEvent { type=EventType.OmnisharpServerMsBuildProjectDiagnostics; constructor(public diagnostics: protocol.MSBuildProjectDiagnostics) { } diff --git a/src/omnisharp/protocol.ts b/src/omnisharp/protocol.ts index a34957001..2b86ba1a3 100644 --- a/src/omnisharp/protocol.ts +++ b/src/omnisharp/protocol.ts @@ -28,6 +28,7 @@ export module Requests { export const TypeLookup = '/typelookup'; export const UpdateBuffer = '/updatebuffer'; export const Metadata = '/metadata'; + export const ReAnalyze = '/reanalyze'; } export namespace WireProtocol { @@ -286,6 +287,18 @@ export interface ProjectInformationResponse { DotNetProject: DotNetProject; } +export enum DiagnosticStatus +{ + Processing = 0, + Ready = 1 +} + +export interface ProjectDiagnosticStatus { + Status: DiagnosticStatus; + ProjectFilePath: string; + Type: "background"; +} + export interface WorkspaceInformationResponse { MsBuild?: MsBuildWorkspaceInformation; DotNet?: DotNetWorkspaceInformation; diff --git a/src/omnisharp/server.ts b/src/omnisharp/server.ts index fbb2c504d..d43d863a4 100644 --- a/src/omnisharp/server.ts +++ b/src/omnisharp/server.ts @@ -56,6 +56,8 @@ module Events { export const ProjectAdded = 'ProjectAdded'; export const ProjectRemoved = 'ProjectRemoved'; + export const ProjectDiagnosticStatus = 'ProjectDiagnosticStatus'; + export const MsBuildProjectDiagnostics = 'MsBuildProjectDiagnostics'; export const TestMessage = 'TestMessage'; @@ -194,6 +196,10 @@ export class OmniSharpServer { return this._addListener(Events.ProjectRemoved, listener, thisArg); } + public onProjectDiagnosticStatus(listener: (e: protocol.ProjectDiagnosticStatus) => any, thisArg?: any) { + return this._addListener(Events.ProjectDiagnosticStatus, listener, thisArg); + } + public onMsBuildProjectDiagnostics(listener: (e: protocol.MSBuildProjectDiagnostics) => any, thisArg?: any) { return this._addListener(Events.MsBuildProjectDiagnostics, listener, thisArg); } @@ -282,6 +288,10 @@ export class OmniSharpServer { this.eventStream.post(new ObservableEvents.OmnisharpServerOnStart()); })); + disposables.add(this.onProjectDiagnosticStatus((message: protocol.ProjectDiagnosticStatus) => + this.eventStream.post(new ObservableEvents.OmnisharpProjectDiagnosticStatus(message)) + )); + disposables.add(this.onProjectConfigurationReceived((message: protocol.ProjectConfigurationMessage) => { this.eventStream.post(new ObservableEvents.ProjectConfiguration(message)); })); @@ -379,7 +389,7 @@ export class OmniSharpServer { return this.stop(); } } - + private onProjectConfigurationReceived(listener: (e: protocol.ProjectConfigurationMessage) => void){ return this._addListener(Events.ProjectConfiguration, listener); } diff --git a/src/omnisharp/utils.ts b/src/omnisharp/utils.ts index 1fda3d7a5..fd0991103 100644 --- a/src/omnisharp/utils.ts +++ b/src/omnisharp/utils.ts @@ -87,6 +87,10 @@ export async function getMetadata(server: OmniSharpServer, request: protocol.Met return server.makeRequest(protocol.Requests.Metadata, request); } +export async function reAnalyze(server: OmniSharpServer, request: any) { + return server.makeRequest(protocol.Requests.ReAnalyze, request); +} + export async function getTestStartInfo(server: OmniSharpServer, request: protocol.V2.GetTestStartInfoRequest) { return server.makeRequest(protocol.V2.Requests.GetTestStartInfo, request); } diff --git a/test/integrationTests/advisor.integration.test.ts b/test/integrationTests/advisor.integration.test.ts index 07edb2791..570fb403f 100644 --- a/test/integrationTests/advisor.integration.test.ts +++ b/test/integrationTests/advisor.integration.test.ts @@ -44,10 +44,10 @@ suite(`Advisor ${testAssetWorkspace.description}`, function () { await testAssetWorkspace.cleanupWorkspace(); }); - test('Advisor.shouldValidateProject returns true when maxProjectFileCountForDiagnosticAnalysis is higher than the file count', async () => { + test('Advisor.shouldValidateAll returns true when maxProjectFileCountForDiagnosticAnalysis is higher than the file count', async () => { await setLimit(1000); - expect(advisor.shouldValidateProject()).to.be.true; + expect(advisor.shouldValidateAll()).to.be.true; }); test('Advisor.shouldValidateFiles returns true when maxProjectFileCountForDiagnosticAnalysis is higher than the file count', async () => { @@ -56,10 +56,10 @@ suite(`Advisor ${testAssetWorkspace.description}`, function () { expect(advisor.shouldValidateFiles()).to.be.true; }); - test('Advisor.shouldValidateProject returns false when maxProjectFileCountForDiagnosticAnalysis is lower than the file count', async () => { + test('Advisor.shouldValidateAll returns false when maxProjectFileCountForDiagnosticAnalysis is lower than the file count', async () => { await setLimit(1); - expect(advisor.shouldValidateProject()).to.be.false; + expect(advisor.shouldValidateAll()).to.be.false; }); test('Advisor.shouldValidateFiles returns true when maxProjectFileCountForDiagnosticAnalysis is lower than the file count', async () => { @@ -68,10 +68,10 @@ suite(`Advisor ${testAssetWorkspace.description}`, function () { expect(advisor.shouldValidateFiles()).to.be.true; }); - test('Advisor.shouldValidateProject returns true when maxProjectFileCountForDiagnosticAnalysis is null', async () => { + test('Advisor.shouldValidateAll returns true when maxProjectFileCountForDiagnosticAnalysis is null', async () => { await setLimit(null); - expect(advisor.shouldValidateProject()).to.be.true; + expect(advisor.shouldValidateAll()).to.be.true; }); test('Advisor.shouldValidateFiles returns true when maxProjectFileCountForDiagnosticAnalysis is null', async () => { diff --git a/test/integrationTests/diagnostics.integration.test.ts b/test/integrationTests/diagnostics.integration.test.ts index 8ae0f5deb..f72965c92 100644 --- a/test/integrationTests/diagnostics.integration.test.ts +++ b/test/integrationTests/diagnostics.integration.test.ts @@ -9,14 +9,20 @@ import * as path from 'path'; import { should, expect } from 'chai'; import { activateCSharpExtension } from './integrationHelpers'; import testAssetWorkspace from './testAssets/testAssetWorkspace'; -import poll, { assertWithPoll } from './poll'; +import { poll, assertWithPoll } from './poll'; const chai = require('chai'); chai.use(require('chai-arrays')); chai.use(require('chai-fs')); +function setDiagnosticWorkspaceLimit(to: number | null) { + let csharpConfig = vscode.workspace.getConfiguration('csharp'); + return csharpConfig.update('maxProjectFileCountForDiagnosticAnalysis', to); +} + suite(`DiagnosticProvider: ${testAssetWorkspace.description}`, function () { let fileUri: vscode.Uri; + let secondaryFileUri: vscode.Uri; suiteSetup(async function () { should(); @@ -25,35 +31,69 @@ suite(`DiagnosticProvider: ${testAssetWorkspace.description}`, function () { await testAssetWorkspace.restore(); let fileName = 'diagnostics.cs'; + let secondaryFileName = 'secondaryDiagnostics.cs'; let projectDirectory = testAssetWorkspace.projects[0].projectDirectoryPath; - let filePath = path.join(projectDirectory, fileName); - fileUri = vscode.Uri.file(filePath); - await vscode.commands.executeCommand("vscode.open", fileUri); + fileUri = vscode.Uri.file(path.join(projectDirectory, fileName)); + secondaryFileUri = vscode.Uri.file(path.join(projectDirectory, secondaryFileName)); }); - suiteTeardown(async () => { - await testAssetWorkspace.cleanupWorkspace(); - }); + suite("small workspace (based on maxProjectFileCountForDiagnosticAnalysis setting)", () => { + suiteSetup(async function () { + should(); + await activateCSharpExtension(); + await testAssetWorkspace.restore(); + await vscode.commands.executeCommand("vscode.open", fileUri); + }); - test("Returns any diagnostics from file", async function () { - await assertWithPoll(() => vscode.languages.getDiagnostics(fileUri), 10 * 1000, 500, - res => expect(res.length).to.be.greaterThan(0)); - }); + test("Returns any diagnostics from file", async function () { + await assertWithPoll(() => vscode.languages.getDiagnostics(fileUri), 10 * 1000, 500, + res => expect(res.length).to.be.greaterThan(0)); + }); + + test("Return unnecessary tag in case of unnesessary using", async function () { + let result = await poll(() => vscode.languages.getDiagnostics(fileUri), 15 * 1000, 500); + + let cs8019 = result.find(x => x.code === "CS8019"); + expect(cs8019).to.not.be.undefined; + expect(cs8019.tags).to.include(vscode.DiagnosticTag.Unnecessary); + }); - test("Return unnecessary tag in case of unnesessary using", async function () { - let result = await poll(() => vscode.languages.getDiagnostics(fileUri), 15*1000, 500); + test("Return fadeout diagnostics like unused variables based on roslyn analyzers", async function () { + let result = await poll(() => vscode.languages.getDiagnostics(fileUri), 15 * 1000, 500, result => result.find(x => x.code === "IDE0059") != undefined); - let cs8019 = result.find(x => x.source == "csharp" && x.code == "CS8019"); - expect(cs8019).to.not.be.undefined; - expect(cs8019.tags).to.include(vscode.DiagnosticTag.Unnecessary); + let ide0059 = result.find(x => x.code === "IDE0059"); + expect(ide0059.tags).to.include(vscode.DiagnosticTag.Unnecessary); + }); + + test("On small workspaces also show/fetch closed document analysis results", async function () { + await assertWithPoll(() => vscode.languages.getDiagnostics(secondaryFileUri), 15 * 1000, 500, res => expect(res.length).to.be.greaterThan(0)); + }); + + suiteTeardown(async () => { + await testAssetWorkspace.cleanupWorkspace(); + }); }); - test("Return fadeout diagnostics like unused usings based on roslyn analyzers", async function () { - let result = await poll(() => vscode.languages.getDiagnostics(fileUri), 15 * 1000, 500, - result => result.find(x => x.source == "csharp" && x.code == "IDE0005") != undefined); + suite("large workspace (based on maxProjectFileCountForDiagnosticAnalysis setting)", () => { + suiteSetup(async function () { + should(); + await setDiagnosticWorkspaceLimit(1); + await testAssetWorkspace.restore(); + await activateCSharpExtension(); + }); + + test("When workspace is count as 'large', then only show/fetch diagnostics from open documents", async function () { + // This is to trigger manual cleanup for diagnostics before test because we modify max project file count on fly. + await vscode.commands.executeCommand("vscode.open", secondaryFileUri); + await vscode.commands.executeCommand("vscode.open", fileUri); + + await assertWithPoll(() => vscode.languages.getDiagnostics(fileUri), 10 * 1000, 500, openFileDiag => expect(openFileDiag.length).to.be.greaterThan(0)); + await assertWithPoll(() => vscode.languages.getDiagnostics(secondaryFileUri), 10 * 1000, 500, secondaryDiag => expect(secondaryDiag.length).to.be.eq(0)); + }); - let ide0005 = result.find(x => x.source == "csharp" && x.code == "IDE0005"); - expect(ide0005.tags).to.include(vscode.DiagnosticTag.Unnecessary); + suiteTeardown(async () => { + await testAssetWorkspace.cleanupWorkspace(); + }); }); }); diff --git a/test/integrationTests/launchConfiguration.integration.test.ts b/test/integrationTests/launchConfiguration.integration.test.ts index 6b6a8f1d3..c3e373212 100644 --- a/test/integrationTests/launchConfiguration.integration.test.ts +++ b/test/integrationTests/launchConfiguration.integration.test.ts @@ -6,10 +6,10 @@ import * as fs from 'async-file'; import * as vscode from 'vscode'; -import poll from './poll'; import { should, expect } from 'chai'; import { activateCSharpExtension } from './integrationHelpers'; import testAssetWorkspace from './testAssets/testAssetWorkspace'; +import { poll } from './poll'; const chai = require('chai'); chai.use(require('chai-arrays')); @@ -31,6 +31,7 @@ suite(`Tasks generation: ${testAssetWorkspace.description}`, function () { }); test("Starting .NET Core Launch (console) from the workspace root should create an Active Debug Session", async () => { + vscode.debug.onDidChangeActiveDebugSession((e) => { expect(vscode.debug.activeDebugSession).not.to.be.undefined; expect(vscode.debug.activeDebugSession.type).to.equal("coreclr"); diff --git a/test/integrationTests/poll.ts b/test/integrationTests/poll.ts index d1b036d9b..b0d428d87 100644 --- a/test/integrationTests/poll.ts +++ b/test/integrationTests/poll.ts @@ -47,7 +47,7 @@ function defaultPollExpression(value: T): boolean { return value !== undefined && ((Array.isArray(value) && value.length > 0) || !Array.isArray(value)); } -export default async function poll( +export async function poll( getValue: () => T, duration: number, step: number, diff --git a/test/integrationTests/reAnalyze.integration.test.ts b/test/integrationTests/reAnalyze.integration.test.ts new file mode 100644 index 000000000..be694ce16 --- /dev/null +++ b/test/integrationTests/reAnalyze.integration.test.ts @@ -0,0 +1,95 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as path from 'path'; + +import { should, expect } from 'chai'; +import { activateCSharpExtension } from './integrationHelpers'; +import testAssetWorkspace from './testAssets/testAssetWorkspace'; +import { poll, assertWithPoll } from './poll'; +import { EventStream } from '../../src/EventStream'; +import { EventType } from '../../src/omnisharp/EventType'; +import { BaseEvent, OmnisharpProjectDiagnosticStatus } from '../../src/omnisharp/loggingEvents'; +import { DiagnosticStatus } from '../../src/omnisharp/protocol'; + +const chai = require('chai'); +chai.use(require('chai-arrays')); +chai.use(require('chai-fs')); + +function listenEvents(stream: EventStream, type: EventType): T[] +{ + let results: T[] = []; + + stream.subscribe((event: BaseEvent) => { + if(event.type === type) + { + results.push(event); + } + }); + + return results; +} + +suite(`ReAnalyze: ${testAssetWorkspace.description}`, function () { + let interfaceUri: vscode.Uri; + let interfaceImplUri: vscode.Uri; + let eventStream: EventStream; + + suiteSetup(async function () { + should(); + eventStream = (await activateCSharpExtension()).eventStream; + await testAssetWorkspace.restore(); + + let projectDirectory = testAssetWorkspace.projects[0].projectDirectoryPath; + interfaceUri = vscode.Uri.file(path.join(projectDirectory, 'ISomeInterface.cs')); + interfaceImplUri = vscode.Uri.file(path.join(projectDirectory, 'SomeInterfaceImpl.cs')); + + await vscode.commands.executeCommand("vscode.open", interfaceImplUri); + await vscode.commands.executeCommand("vscode.open", interfaceUri); + }); + + suiteTeardown(async () => { + await testAssetWorkspace.cleanupWorkspace(); + }); + + test("When interface is manually renamed, then return correct analysis after re-analysis of project", async function () { + let diagnosticStatusEvents = listenEvents(eventStream, EventType.ProjectDiagnosticStatus); + + await vscode.commands.executeCommand("vscode.open", interfaceUri); + + let editor = vscode.window.activeTextEditor; + + await editor.edit(editorBuilder => editorBuilder.replace(new vscode.Range(2, 0, 2, 50), 'public interface ISomeInterfaceRenamedNow')); + + await vscode.commands.executeCommand('o.reanalyze.currentProject', interfaceImplUri); + + await poll(() => diagnosticStatusEvents, 15*1000, 500, r => r.find(x => x.message.Status === DiagnosticStatus.Ready) !== undefined); + + await assertWithPoll( + () => vscode.languages.getDiagnostics(interfaceImplUri), + 15*1000, + 500, + res => expect(res.find(x => x.message.includes("CS0246")))); + }); + + test("When re-analyze of project is executed then eventually get notified about them.", async function () { + let diagnosticStatusEvents = listenEvents(eventStream, EventType.ProjectDiagnosticStatus); + + await vscode.commands.executeCommand('o.reanalyze.currentProject', interfaceImplUri); + + await poll(() => diagnosticStatusEvents, 15*1000, 500, r => r.find(x => x.message.Status === DiagnosticStatus.Processing) != undefined); + await poll(() => diagnosticStatusEvents, 15*1000, 500, r => r.find(x => x.message.Status === DiagnosticStatus.Ready) != undefined); + }); + + test("When re-analyze of all projects is executed then eventually get notified about them.", async function () { + let diagnosticStatusEvents = listenEvents(eventStream, EventType.ProjectDiagnosticStatus); + + await vscode.commands.executeCommand('o.reanalyze.allProjects', interfaceImplUri); + + await poll(() => diagnosticStatusEvents, 15*1000, 500, r => r.find(x => x.message.Status === DiagnosticStatus.Processing) != undefined); + await poll(() => diagnosticStatusEvents, 15*1000, 500, r => r.find(x => x.message.Status === DiagnosticStatus.Ready) != undefined); + }); +}); diff --git a/test/integrationTests/testAssets/singleCsproj/ISomeInterface.cs b/test/integrationTests/testAssets/singleCsproj/ISomeInterface.cs new file mode 100644 index 000000000..4c2da47f4 --- /dev/null +++ b/test/integrationTests/testAssets/singleCsproj/ISomeInterface.cs @@ -0,0 +1,6 @@ +namespace ReAnalyze +{ + public interface ISomeInterface + { + } +} \ No newline at end of file diff --git a/test/integrationTests/testAssets/singleCsproj/SomeInterfaceImpl.cs b/test/integrationTests/testAssets/singleCsproj/SomeInterfaceImpl.cs new file mode 100644 index 000000000..ce6b9fa31 --- /dev/null +++ b/test/integrationTests/testAssets/singleCsproj/SomeInterfaceImpl.cs @@ -0,0 +1,6 @@ +namespace ReAnalyze +{ + public class SomeInterfaceImpl: ISomeInterface + { + } +} \ No newline at end of file diff --git a/test/integrationTests/testAssets/singleCsproj/secondaryDiagnostics.cs b/test/integrationTests/testAssets/singleCsproj/secondaryDiagnostics.cs new file mode 100644 index 000000000..1a866d573 --- /dev/null +++ b/test/integrationTests/testAssets/singleCsproj/secondaryDiagnostics.cs @@ -0,0 +1,12 @@ +using System.IO; + +namespace Foo +{ + public class SecondaryDiagnostics + { + public void FooBarBar() + { + var notUsed = 3; + } + } +} \ No newline at end of file diff --git a/test/integrationTests/testAssets/slnWithCsproj/src/app/ISomeInterface.cs b/test/integrationTests/testAssets/slnWithCsproj/src/app/ISomeInterface.cs new file mode 100644 index 000000000..4c2da47f4 --- /dev/null +++ b/test/integrationTests/testAssets/slnWithCsproj/src/app/ISomeInterface.cs @@ -0,0 +1,6 @@ +namespace ReAnalyze +{ + public interface ISomeInterface + { + } +} \ No newline at end of file diff --git a/test/integrationTests/testAssets/slnWithCsproj/src/app/SomeInterfaceImpl.cs b/test/integrationTests/testAssets/slnWithCsproj/src/app/SomeInterfaceImpl.cs new file mode 100644 index 000000000..ce6b9fa31 --- /dev/null +++ b/test/integrationTests/testAssets/slnWithCsproj/src/app/SomeInterfaceImpl.cs @@ -0,0 +1,6 @@ +namespace ReAnalyze +{ + public class SomeInterfaceImpl: ISomeInterface + { + } +} \ No newline at end of file diff --git a/test/integrationTests/testAssets/slnWithCsproj/src/app/secondaryDiagnostics.cs b/test/integrationTests/testAssets/slnWithCsproj/src/app/secondaryDiagnostics.cs new file mode 100644 index 000000000..1a866d573 --- /dev/null +++ b/test/integrationTests/testAssets/slnWithCsproj/src/app/secondaryDiagnostics.cs @@ -0,0 +1,12 @@ +using System.IO; + +namespace Foo +{ + public class SecondaryDiagnostics + { + public void FooBarBar() + { + var notUsed = 3; + } + } +} \ No newline at end of file diff --git a/test/unitTests/logging/BackgroundWorkStatusBarObserver.test.ts b/test/unitTests/logging/BackgroundWorkStatusBarObserver.test.ts new file mode 100644 index 000000000..6e0503a58 --- /dev/null +++ b/test/unitTests/logging/BackgroundWorkStatusBarObserver.test.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { StatusBarItem } from '../../../src/vscodeAdapter'; +import { OmnisharpProjectDiagnosticStatus } from '../../../src/omnisharp/loggingEvents'; +import { BackgroundWorkStatusBarObserver } from '../../../src/observers/BackgroundWorkStatusBarObserver'; +import { DiagnosticStatus } from '../../../src/omnisharp/protocol'; + +suite('BackgroundWorkStatusBarObserver', () => { + suiteSetup(() => should()); + + let showCalled: boolean; + let hideCalled: boolean; + let statusBarItem = { + show: () => { showCalled = true; }, + hide: () => { hideCalled = true; } + }; + let observer = new BackgroundWorkStatusBarObserver(statusBarItem); + + setup(() => { + showCalled = false; + hideCalled = false; + }); + + test('OmnisharpProjectDiagnosticStatus.Processing: Show processing message', () => { + let event = new OmnisharpProjectDiagnosticStatus({ Status: DiagnosticStatus.Processing, ProjectFilePath: "foo.csproj", Type: "background" }); + observer.post(event); + expect(hideCalled).to.be.false; + expect(showCalled).to.be.true; + expect(statusBarItem.text).to.contain('Analyzing'); + }); + + test('OmnisharpProjectDiagnosticStatus.Ready: Hide processing message', () => { + let event = new OmnisharpProjectDiagnosticStatus({ Status: DiagnosticStatus.Ready, ProjectFilePath: "foo.csproj", Type: "background" }); + observer.post(event); + expect(hideCalled).to.be.true; + expect(showCalled).to.be.false; + expect(statusBarItem.text).to.be.undefined; + }); +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 58caa9bb2..402904926 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ "skipLibCheck": true, "noImplicitThis": true, "noUnusedLocals": true, - "noFallthroughCasesInSwitch": true, + "noFallthroughCasesInSwitch": true }, "exclude": [ "syntaxes",