Skip to content

Commit

Permalink
Merge pull request #3089 from savpek/feature/analyze-commands
Browse files Browse the repository at this point in the history
Re-Analyze project or all projects
  • Loading branch information
JoeRobich authored Dec 5, 2019
2 parents 9f92a69 + 431326b commit 70b37a6
Show file tree
Hide file tree
Showing 24 changed files with 454 additions and 142 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 13 additions & 0 deletions src/features/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => { }));
Expand Down Expand Up @@ -129,6 +132,16 @@ async function pickProjectAndDotnetRestore(server: OmniSharpServer, eventStream:
}
}

async function reAnalyzeAllProjects(server: OmniSharpServer, eventStream: EventStream): Promise<void> {
await serverUtils.reAnalyze(server, {});
}

async function reAnalyzeCurrentProject(server: OmniSharpServer, eventStream: EventStream): Promise<void> {
await serverUtils.reAnalyze(server, {
fileName: vscode.window.activeTextEditor.document.uri.fsPath
});
}

async function dotnetRestoreAllProjects(server: OmniSharpServer, eventStream: EventStream): Promise<void> {
let descriptors = await getProjectDescriptors(server);
eventStream.post(new CommandDotNetRestoreStart());
Expand Down
210 changes: 102 additions & 108 deletions src/features/diagnosticsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -43,7 +46,7 @@ export class Advisor {
&& !this._isRestoringPackages();
}

public shouldValidateProject(): boolean {
public shouldValidateAll(): boolean {
return this._isServerStarted()
&& !this._isRestoringPackages()
&& !this._isOverFileLimit();
Expand Down Expand Up @@ -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<vscode.TextDocument>();
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();
}

Expand All @@ -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;
Expand All @@ -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 }[] {
Expand All @@ -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 } {
Expand Down
9 changes: 7 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CSharpExtensionExports> {

Expand Down Expand Up @@ -91,14 +92,18 @@ export async function activate(context: vscode.ExtensionContext): Promise<CSharp
let errorMessageObserver = new ErrorMessageObserver(vscode);
eventStream.subscribe(errorMessageObserver.post);

let omnisharpStatusBar = new StatusBarItemAdapter(vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, Number.MIN_VALUE));
let omnisharpStatusBar = new StatusBarItemAdapter(vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, Number.MIN_VALUE+2));
let omnisharpStatusBarObserver = new OmnisharpStatusBarObserver(omnisharpStatusBar);
eventStream.subscribe(omnisharpStatusBarObserver.post);

let projectStatusBar = new StatusBarItemAdapter(vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left));
let projectStatusBar = new StatusBarItemAdapter(vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, Number.MIN_VALUE+1));
let projectStatusBarObserver = new ProjectStatusBarObserver(projectStatusBar);
eventStream.subscribe(projectStatusBarObserver.post);

let backgroundWorkStatusBar = new StatusBarItemAdapter(vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, Number.MIN_VALUE));
let backgroundWorkStatusBarObserver = new BackgroundWorkStatusBarObserver(backgroundWorkStatusBar);
eventStream.subscribe(backgroundWorkStatusBarObserver.post);

let openURLObserver = new OpenURLObserver(vscode);
eventStream.subscribe(openURLObserver.post);

Expand Down
Loading

0 comments on commit 70b37a6

Please sign in to comment.