提交 9fb32293 编写于 作者: M Matt Bierner

Use single diagnostic collection for js and ts

Refactors the ts DiagnosticManager to be shared between language providers.  To do this:

- Make sure we always maintain a complete list of diagnostics in the extension. But only update the vscode.DiagnosticCollection with the ones we care about

- Add the concept of a diagnostic language. This is needed now that we only have a single collection. Use the diagnostic language to determine which diagnostics to filter out using `typescript.validate` and `javascript.validate`

- Add a diagnosticSetting class to track settings for different languages (js and ts)

Fixes #54359
上级 ab53222c
......@@ -5,51 +5,146 @@
import * as vscode from 'vscode';
import { ResourceMap } from '../utils/resourceMap';
import { DiagnosticLanguage, allDiagnosticLangauges } from '../utils/languageDescription';
export class DiagnosticSet {
private _map = new ResourceMap<vscode.Diagnostic[]>();
export enum DiagnosticKind {
Syntax,
Semantic,
Suggestion
}
public set(
file: vscode.Uri,
const allDiagnosticKinds = [DiagnosticKind.Syntax, DiagnosticKind.Semantic, DiagnosticKind.Suggestion];
class FileDiagnostics {
private readonly _diagnostics = new Map<DiagnosticKind, vscode.Diagnostic[]>();
constructor(
public readonly file: vscode.Uri,
public language: DiagnosticLanguage
) { }
public isEmpty(): boolean {
return allDiagnosticKinds.every(kind => {
const diagnostics = this._diagnostics.get(kind);
return !!(diagnostics && diagnostics.length);
});
}
public updateDiagnostics(
language: DiagnosticLanguage,
kind: DiagnosticKind,
diagnostics: vscode.Diagnostic[]
) {
this._map.set(file, diagnostics);
): boolean {
if (language !== this.language) {
this._diagnostics.clear();
this.language = language;
}
if (diagnostics.length === 0) {
const existing = this._diagnostics.get(kind);
if (!existing || existing && existing.length === 0) {
// No need to update
return false;
}
}
this._diagnostics.set(kind, diagnostics);
return true;
}
public get(file: vscode.Uri): vscode.Diagnostic[] {
return this._map.get(file) || [];
public getDiagnostics(settings: DiagnosticSettings): vscode.Diagnostic[] {
if (!settings.getValidate(this.language)) {
return [];
}
return [
...(this._diagnostics.get(DiagnosticKind.Syntax) || []),
...(this._diagnostics.get(DiagnosticKind.Semantic) || []),
...this.getSuggestionDiagnostics(settings),
];
}
public clear(): void {
this._map = new ResourceMap<vscode.Diagnostic[]>();
private getSuggestionDiagnostics(settings: DiagnosticSettings) {
if (!this._diagnostics.get(DiagnosticKind.Suggestion)) {
return [];
}
const enableSuggestions = settings.getEnableSuggestions(this.language);
return this._diagnostics.get(DiagnosticKind.Suggestion)!.filter(x => {
if (enableSuggestions) {
// Still show unused
return x.tags && x.tags.indexOf(vscode.DiagnosticTag.Unnecessary) !== -1;
}
return true;
});
}
}
export enum DiagnosticKind {
Syntax,
Semantic,
Suggestion
interface LangaugeDiagnosticSettings {
readonly validate: boolean;
readonly enableSuggestions: boolean;
}
const allDiagnosticKinds = [DiagnosticKind.Syntax, DiagnosticKind.Semantic, DiagnosticKind.Suggestion];
class DiagnosticSettings {
private static readonly defaultSettings: LangaugeDiagnosticSettings = {
validate: true,
enableSuggestions: true
};
export class DiagnosticsManager {
private readonly _languageSettings = new Map<DiagnosticLanguage, LangaugeDiagnosticSettings>();
constructor() {
for (const language of allDiagnosticLangauges) {
this._languageSettings.set(language, DiagnosticSettings.defaultSettings);
}
}
public getValidate(language: DiagnosticLanguage): boolean {
return this.get(language).validate;
}
public setValidate(language: DiagnosticLanguage, value: boolean): boolean {
return this.update(language, settings => ({
validate: value,
enableSuggestions: settings.enableSuggestions
}));
}
private readonly _diagnostics = new Map<DiagnosticKind, DiagnosticSet>();
public getEnableSuggestions(language: DiagnosticLanguage): boolean {
return this.get(language).enableSuggestions;
}
public setEnableSuggestions(language: DiagnosticLanguage, value: boolean): boolean {
return this.update(language, settings => ({
validate: settings.validate,
enableSuggestions: value
}));
}
private get(language: DiagnosticLanguage): LangaugeDiagnosticSettings {
return this._languageSettings.get(language) || DiagnosticSettings.defaultSettings;
}
private update(language: DiagnosticLanguage, f: (x: LangaugeDiagnosticSettings) => LangaugeDiagnosticSettings): boolean {
const currentSettings = this.get(language);
const newSettings = f(currentSettings);
this._languageSettings.set(language, newSettings);
return currentSettings.validate === newSettings.validate
&& currentSettings.enableSuggestions && currentSettings.enableSuggestions;
}
}
export class DiagnosticsManager {
private readonly _diagnostics = new ResourceMap<FileDiagnostics>();
private readonly _settings = new DiagnosticSettings();
private readonly _currentDiagnostics: vscode.DiagnosticCollection;
private _pendingUpdates = new ResourceMap<any>();
private _validate: boolean = true;
private _enableSuggestions: boolean = true;
private readonly updateDelay = 50;
private readonly _updateDelay = 50;
constructor(
owner: string
) {
for (const kind of allDiagnosticKinds) {
this._diagnostics.set(kind, new DiagnosticSet());
}
this._currentDiagnostics = vscode.languages.createDiagnosticCollection(owner);
}
......@@ -64,63 +159,56 @@ export class DiagnosticsManager {
public reInitialize(): void {
this._currentDiagnostics.clear();
for (const diagnosticSet of this._diagnostics.values()) {
diagnosticSet.clear();
}
this._diagnostics.clear();
}
public set validate(value: boolean) {
if (this._validate === value) {
return;
}
this._validate = value;
if (!value) {
this._currentDiagnostics.clear();
public setValidate(language: DiagnosticLanguage, value: boolean) {
const didUpdate = this._settings.setValidate(language, value);
if (didUpdate) {
this.rebuild();
}
}
public set enableSuggestions(value: boolean) {
if (this._enableSuggestions === value) {
return;
}
this._enableSuggestions = value;
if (!value) {
this._currentDiagnostics.clear();
public setEnableSuggestions(language: DiagnosticLanguage, value: boolean) {
const didUpdate = this._settings.setEnableSuggestions(language, value);
if (didUpdate) {
this.rebuild();
}
}
public diagnosticsReceived(
kind: DiagnosticKind,
public updateDiagnostics(
file: vscode.Uri,
language: DiagnosticLanguage,
kind: DiagnosticKind,
diagnostics: vscode.Diagnostic[]
): void {
const collection = this._diagnostics.get(kind);
if (!collection) {
return;
}
if (diagnostics.length === 0) {
const existing = collection.get(file);
if (existing.length === 0) {
// No need to update
return;
}
let didUpdate = false;
const entry = this._diagnostics.get(file);
if (entry) {
didUpdate = entry.updateDiagnostics(language, kind, diagnostics);
} else if (diagnostics.length) {
const fileDiagnostics = new FileDiagnostics(file, language);
fileDiagnostics.updateDiagnostics(language, kind, diagnostics);
this._diagnostics.set(file, fileDiagnostics);
didUpdate = true;
}
collection.set(file, diagnostics);
this.scheduleDiagnosticsUpdate(file);
if (didUpdate) {
this.scheduleDiagnosticsUpdate(file);
}
}
public configFileDiagnosticsReceived(file: vscode.Uri, diagnostics: vscode.Diagnostic[]): void {
public configFileDiagnosticsReceived(
file: vscode.Uri,
diagnostics: vscode.Diagnostic[]
): void {
this._currentDiagnostics.set(file, diagnostics);
}
public delete(resource: vscode.Uri): void {
this._currentDiagnostics.delete(resource);
this._diagnostics.delete(resource);
}
public getDiagnostics(file: vscode.Uri): vscode.Diagnostic[] {
......@@ -129,35 +217,24 @@ export class DiagnosticsManager {
private scheduleDiagnosticsUpdate(file: vscode.Uri) {
if (!this._pendingUpdates.has(file)) {
this._pendingUpdates.set(file, setTimeout(() => this.updateCurrentDiagnostics(file), this.updateDelay));
this._pendingUpdates.set(file, setTimeout(() => this.updateCurrentDiagnostics(file), this._updateDelay));
}
}
private updateCurrentDiagnostics(file: vscode.Uri) {
private updateCurrentDiagnostics(file: vscode.Uri): void {
if (this._pendingUpdates.has(file)) {
clearTimeout(this._pendingUpdates.get(file));
this._pendingUpdates.delete(file);
}
if (!this._validate) {
return;
}
const allDiagnostics = [
...this._diagnostics.get(DiagnosticKind.Syntax)!.get(file),
...this._diagnostics.get(DiagnosticKind.Semantic)!.get(file),
...this.getSuggestionDiagnostics(file),
];
this._currentDiagnostics.set(file, allDiagnostics);
const fileDiagnostics = this._diagnostics.get(file);
this._currentDiagnostics.set(file, fileDiagnostics ? fileDiagnostics.getDiagnostics(this._settings) : []);
}
private getSuggestionDiagnostics(file: vscode.Uri) {
return this._diagnostics.get(DiagnosticKind.Suggestion)!.get(file).filter(x => {
if (!this._enableSuggestions) {
// Still show unused
return x.tags && x.tags.indexOf(vscode.DiagnosticTag.Unnecessary) !== -1;
}
return true;
});
private rebuild(): void {
this._currentDiagnostics.clear();
for (const fileDiagnostic of Array.from(this._diagnostics.values)) {
this._currentDiagnostics.set(fileDiagnostic.file, fileDiagnostic.getDiagnostics(this._settings));
}
}
}
\ No newline at end of file
......@@ -6,7 +6,7 @@
import { basename } from 'path';
import * as vscode from 'vscode';
import { CachedNavTreeResponse } from './features/baseCodeLensProvider';
import { DiagnosticKind, DiagnosticsManager } from './features/diagnostics';
import { DiagnosticKind } from './features/diagnostics';
import FileConfigurationManager from './features/fileConfigurationManager';
import TypeScriptServiceClient from './typescriptServiceClient';
import { CommandManager } from './utils/commandManager';
......@@ -22,11 +22,6 @@ const validateSetting = 'validate.enable';
const suggestionSetting = 'suggestionActions.enabled';
export default class LanguageProvider {
private readonly diagnosticsManager: DiagnosticsManager;
private _validate: boolean = true;
private _enableSuggestionDiagnostics: boolean = true;
private readonly disposables: vscode.Disposable[] = [];
constructor(
......@@ -37,12 +32,6 @@ export default class LanguageProvider {
private readonly typingsStatus: TypingsStatus,
private readonly fileConfigurationManager: FileConfigurationManager
) {
this.client.bufferSyncSupport.onDelete(resource => {
this.diagnosticsManager.delete(resource);
}, null, this.disposables);
this.diagnosticsManager = new DiagnosticsManager(description.diagnosticOwner);
vscode.workspace.onDidChangeConfiguration(this.configurationChanged, this, this.disposables);
this.configurationChanged();
......@@ -53,8 +42,6 @@ export default class LanguageProvider {
public dispose(): void {
disposeAll(this.disposables);
this.diagnosticsManager.dispose();
}
@memoize
......@@ -85,7 +72,7 @@ export default class LanguageProvider {
this.disposables.push((await import('./features/implementationsCodeLens')).register(selector, this.description.id, this.client, cachedResponse));
this.disposables.push((await import('./features/jsDocCompletions')).register(selector, this.client, this.commandManager));
this.disposables.push((await import('./features/organizeImports')).register(selector, this.client, this.commandManager, this.fileConfigurationManager, this.telemetryReporter));
this.disposables.push((await import('./features/quickFix')).register(selector, this.client, this.fileConfigurationManager, this.commandManager, this.diagnosticsManager, this.telemetryReporter));
this.disposables.push((await import('./features/quickFix')).register(selector, this.client, this.fileConfigurationManager, this.commandManager, this.client.diagnosticsManager, this.telemetryReporter));
this.disposables.push((await import('./features/refactor')).register(selector, this.client, this.fileConfigurationManager, this.commandManager, this.telemetryReporter));
this.disposables.push((await import('./features/references')).register(selector, this.client));
this.disposables.push((await import('./features/referencesCodeLens')).register(selector, this.description.id, this.client, cachedResponse));
......@@ -120,30 +107,15 @@ export default class LanguageProvider {
}
private updateValidate(value: boolean) {
if (this._validate === value) {
return;
}
this._validate = value;
this.diagnosticsManager.validate = value;
if (value) {
this.triggerAllDiagnostics();
}
this.client.diagnosticsManager.setValidate(this._diagnosticLanguage, value);
}
private updateSuggestionDiagnostics(value: boolean) {
if (this._enableSuggestionDiagnostics === value) {
return;
}
this._enableSuggestionDiagnostics = value;
this.diagnosticsManager.enableSuggestions = value;
if (value) {
this.triggerAllDiagnostics();
}
this.client.diagnosticsManager.setEnableSuggestions(this._diagnosticLanguage, value);
}
public reInitialize(): void {
this.diagnosticsManager.reInitialize();
this.client.diagnosticsManager.reInitialize();
}
public triggerAllDiagnostics(): void {
......@@ -153,7 +125,7 @@ export default class LanguageProvider {
public diagnosticsReceived(diagnosticsKind: DiagnosticKind, file: vscode.Uri, diagnostics: (vscode.Diagnostic & { reportUnnecessary: any })[]): void {
const config = vscode.workspace.getConfiguration(this.id, file);
const reportUnnecessary = config.get<boolean>('showUnused', true);
this.diagnosticsManager.diagnosticsReceived(diagnosticsKind, file, diagnostics.filter(diag => {
this.client.diagnosticsManager.updateDiagnostics(file, this._diagnosticLanguage, diagnosticsKind, diagnostics.filter(diag => {
if (!reportUnnecessary) {
diag.tags = undefined;
if (diag.reportUnnecessary && diag.severity === vscode.DiagnosticSeverity.Hint) {
......@@ -165,6 +137,10 @@ export default class LanguageProvider {
}
public configFileDiagnosticsReceived(file: vscode.Uri, diagnostics: vscode.Diagnostic[]): void {
this.diagnosticsManager.configFileDiagnosticsReceived(file, diagnostics);
this.client.diagnosticsManager.configFileDiagnosticsReceived(file, diagnostics);
}
private get _diagnosticLanguage() {
return this.description.diagnosticLanguage;
}
}
\ No newline at end of file
......@@ -19,7 +19,7 @@ import TypeScriptServiceClient from './typescriptServiceClient';
import API from './utils/api';
import { CommandManager } from './utils/commandManager';
import { disposeAll } from './utils/dispose';
import { LanguageDescription } from './utils/languageDescription';
import { LanguageDescription, DiagnosticLanguage } from './utils/languageDescription';
import LogDirectoryProvider from './utils/logDirectoryProvider';
import { TypeScriptServerPlugin } from './utils/plugins';
import * as typeConverters from './utils/typeConverters';
......@@ -119,7 +119,8 @@ export default class TypeScriptServiceClientHost {
const description: LanguageDescription = {
id: 'typescript-plugins',
modeIds: Array.from(languages.values()),
diagnosticSource: 'ts-plugins',
diagnosticSource: 'ts-plugin',
diagnosticLanguage: DiagnosticLanguage.TypeScript,
diagnosticOwner: 'typescript',
isExternal: true
};
......
......@@ -9,7 +9,7 @@ import * as path from 'path';
import { CancellationToken, commands, Disposable, env, EventEmitter, Memento, MessageItem, Uri, window, workspace } from 'vscode';
import * as nls from 'vscode-nls';
import BufferSyncSupport from './features/bufferSyncSupport';
import { DiagnosticKind } from './features/diagnostics';
import { DiagnosticKind, DiagnosticsManager } from './features/diagnostics';
import * as Proto from './protocol';
import { ITypeScriptServiceClient } from './typescriptService';
import API from './utils/api';
......@@ -197,6 +197,7 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
private readonly disposables: Disposable[] = [];
public readonly bufferSyncSupport: BufferSyncSupport;
public readonly diagnosticsManager: DiagnosticsManager;
constructor(
private readonly workspaceState: Memento,
......@@ -231,6 +232,11 @@ export default class TypeScriptServiceClient implements ITypeScriptServiceClient
this.bufferSyncSupport = new BufferSyncSupport(this, allModeIds);
this.onReady(() => { this.bufferSyncSupport.listen(); });
this.diagnosticsManager = new DiagnosticsManager('typescript');
this.bufferSyncSupport.onDelete(resource => {
this.diagnosticsManager.delete(resource);
}, null, this.disposables);
workspace.onDidChangeConfiguration(() => {
const oldConfiguration = this._configuration;
this._configuration = TypeScriptServiceConfiguration.loadFromWorkspace();
......
......@@ -4,26 +4,36 @@
*--------------------------------------------------------------------------------------------*/
import * as languageModeIds from './languageModeIds';
export enum DiagnosticLanguage {
JavaScript,
TypeScript
}
export const allDiagnosticLangauges = [DiagnosticLanguage.JavaScript, DiagnosticLanguage.TypeScript];
export interface LanguageDescription {
readonly id: string;
readonly diagnosticOwner: string;
readonly diagnosticSource: string;
readonly diagnosticLanguage: DiagnosticLanguage;
readonly modeIds: string[];
readonly configFile?: string;
readonly isExternal?: boolean;
readonly diagnosticOwner: string;
}
export const standardLanguageDescriptions: LanguageDescription[] = [
{
id: 'typescript',
diagnosticSource: 'ts',
diagnosticOwner: 'typescript',
diagnosticSource: 'ts',
diagnosticLanguage: DiagnosticLanguage.TypeScript,
modeIds: [languageModeIds.typescript, languageModeIds.typescriptreact],
configFile: 'tsconfig.json'
}, {
id: 'javascript',
diagnosticSource: 'ts',
diagnosticOwner: 'typescript',
diagnosticSource: 'ts',
diagnosticLanguage: DiagnosticLanguage.JavaScript,
modeIds: [languageModeIds.javascript, languageModeIds.javascriptreact],
configFile: 'jsconfig.json'
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册