mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-24 10:38:59 +01:00
[typescript-language-features] Region-based semantic diagnostics for TypeScript (#208713)
* WIP * invalidate diagnostics in range * check whether should use region based diagnostics * add ts-expect-errors * make region opt off by default * bump to expected 5.6 * update comments to refer to 5.6 * make region diagnostics on by default for insiders
This commit is contained in:
committed by
GitHub
parent
7717059b2e
commit
878af0771b
@@ -124,6 +124,7 @@ export interface TypeScriptServiceConfiguration {
|
||||
readonly localNodePath: string | null;
|
||||
readonly globalNodePath: string | null;
|
||||
readonly workspaceSymbolsExcludeLibrarySymbols: boolean;
|
||||
readonly enableRegionDiagnostics: boolean;
|
||||
}
|
||||
|
||||
export function areServiceConfigurationsEqual(a: TypeScriptServiceConfiguration, b: TypeScriptServiceConfiguration): boolean {
|
||||
@@ -162,6 +163,7 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu
|
||||
localNodePath: this.readLocalNodePath(configuration),
|
||||
globalNodePath: this.readGlobalNodePath(configuration),
|
||||
workspaceSymbolsExcludeLibrarySymbols: this.readWorkspaceSymbolsExcludeLibrarySymbols(configuration),
|
||||
enableRegionDiagnostics: this.readEnableRegionDiagnostics(configuration),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -267,4 +269,8 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu
|
||||
private readWebTypeAcquisition(configuration: vscode.WorkspaceConfiguration): boolean {
|
||||
return configuration.get<boolean>('typescript.tsserver.web.typeAcquisition.enabled', false);
|
||||
}
|
||||
|
||||
private readEnableRegionDiagnostics(configuration: vscode.WorkspaceConfiguration): boolean {
|
||||
return configuration.get<boolean>('typescript.tsserver.enableRegionDiagnostics', true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ export const enum DiagnosticKind {
|
||||
Syntax,
|
||||
Semantic,
|
||||
Suggestion,
|
||||
RegionSemantic,
|
||||
}
|
||||
|
||||
class FileDiagnostics {
|
||||
@@ -48,7 +49,8 @@ class FileDiagnostics {
|
||||
public updateDiagnostics(
|
||||
language: DiagnosticLanguage,
|
||||
kind: DiagnosticKind,
|
||||
diagnostics: ReadonlyArray<vscode.Diagnostic>
|
||||
diagnostics: ReadonlyArray<vscode.Diagnostic>,
|
||||
ranges: ReadonlyArray<vscode.Range> | undefined
|
||||
): boolean {
|
||||
if (language !== this.language) {
|
||||
this._diagnostics.clear();
|
||||
@@ -61,6 +63,9 @@ class FileDiagnostics {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (kind === DiagnosticKind.RegionSemantic) {
|
||||
return this.updateRegionDiagnostics(diagnostics, ranges!);
|
||||
}
|
||||
this._diagnostics.set(kind, diagnostics);
|
||||
return true;
|
||||
}
|
||||
@@ -83,6 +88,23 @@ class FileDiagnostics {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ranges The ranges whose diagnostics were updated.
|
||||
*/
|
||||
private updateRegionDiagnostics(
|
||||
diagnostics: ReadonlyArray<vscode.Diagnostic>,
|
||||
ranges: ReadonlyArray<vscode.Range>): boolean {
|
||||
if (!this._diagnostics.get(DiagnosticKind.Semantic)) {
|
||||
this._diagnostics.set(DiagnosticKind.Semantic, diagnostics);
|
||||
return true;
|
||||
}
|
||||
const oldDiagnostics = this._diagnostics.get(DiagnosticKind.Semantic)!;
|
||||
const newDiagnostics = oldDiagnostics.filter(diag => !ranges.some(range => diag.range.intersection(range)));
|
||||
newDiagnostics.push(...diagnostics);
|
||||
this._diagnostics.set(DiagnosticKind.Semantic, newDiagnostics);
|
||||
return true;
|
||||
}
|
||||
|
||||
private getSuggestionDiagnostics(settings: DiagnosticSettings) {
|
||||
const enableSuggestions = settings.getEnableSuggestions(this.language);
|
||||
return this.get(DiagnosticKind.Suggestion).filter(x => {
|
||||
@@ -284,15 +306,16 @@ export class DiagnosticsManager extends Disposable {
|
||||
file: vscode.Uri,
|
||||
language: DiagnosticLanguage,
|
||||
kind: DiagnosticKind,
|
||||
diagnostics: ReadonlyArray<vscode.Diagnostic>
|
||||
diagnostics: ReadonlyArray<vscode.Diagnostic>,
|
||||
ranges: ReadonlyArray<vscode.Range> | undefined,
|
||||
): void {
|
||||
let didUpdate = false;
|
||||
const entry = this._diagnostics.get(file);
|
||||
if (entry) {
|
||||
didUpdate = entry.updateDiagnostics(language, kind, diagnostics);
|
||||
didUpdate = entry.updateDiagnostics(language, kind, diagnostics, ranges);
|
||||
} else if (diagnostics.length) {
|
||||
const fileDiagnostics = new FileDiagnostics(file, language);
|
||||
fileDiagnostics.updateDiagnostics(language, kind, diagnostics);
|
||||
fileDiagnostics.updateDiagnostics(language, kind, diagnostics, ranges);
|
||||
this._diagnostics.set(file, fileDiagnostics);
|
||||
didUpdate = true;
|
||||
}
|
||||
|
||||
@@ -138,7 +138,11 @@ export default class LanguageProvider extends Disposable {
|
||||
this.client.bufferSyncSupport.requestAllDiagnostics();
|
||||
}
|
||||
|
||||
public diagnosticsReceived(diagnosticsKind: DiagnosticKind, file: vscode.Uri, diagnostics: (vscode.Diagnostic & { reportUnnecessary: any; reportDeprecated: any })[]): void {
|
||||
public diagnosticsReceived(
|
||||
diagnosticsKind: DiagnosticKind,
|
||||
file: vscode.Uri,
|
||||
diagnostics: (vscode.Diagnostic & { reportUnnecessary: any; reportDeprecated: any })[],
|
||||
ranges: vscode.Range[] | undefined): void {
|
||||
if (diagnosticsKind !== DiagnosticKind.Syntax && !this.client.hasCapabilityForResource(file, ClientCapability.Semantic)) {
|
||||
return;
|
||||
}
|
||||
@@ -175,7 +179,7 @@ export default class LanguageProvider extends Disposable {
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}));
|
||||
}), ranges);
|
||||
}
|
||||
|
||||
public configFileDiagnosticsReceived(file: vscode.Uri, diagnostics: vscode.Diagnostic[]): void {
|
||||
|
||||
@@ -275,12 +275,12 @@ class SyncedBufferMap extends ResourceMap<SyncedBuffer> {
|
||||
}
|
||||
|
||||
class PendingDiagnostics extends ResourceMap<number> {
|
||||
public getOrderedFileSet(): ResourceMap<void> {
|
||||
public getOrderedFileSet(): ResourceMap<void | vscode.Range[]> {
|
||||
const orderedResources = Array.from(this.entries())
|
||||
.sort((a, b) => a.value - b.value)
|
||||
.map(entry => entry.resource);
|
||||
|
||||
const map = new ResourceMap<void>(this._normalizePath, this.config);
|
||||
const map = new ResourceMap<void | vscode.Range[]>(this._normalizePath, this.config);
|
||||
for (const resource of orderedResources) {
|
||||
map.set(resource, undefined);
|
||||
}
|
||||
@@ -292,7 +292,7 @@ class GetErrRequest {
|
||||
|
||||
public static executeGetErrRequest(
|
||||
client: ITypeScriptServiceClient,
|
||||
files: ResourceMap<void>,
|
||||
files: ResourceMap<void | vscode.Range[]>,
|
||||
onDone: () => void
|
||||
) {
|
||||
return new GetErrRequest(client, files, onDone);
|
||||
@@ -303,7 +303,7 @@ class GetErrRequest {
|
||||
|
||||
private constructor(
|
||||
private readonly client: ITypeScriptServiceClient,
|
||||
public readonly files: ResourceMap<void>,
|
||||
public readonly files: ResourceMap<void | vscode.Range[]>,
|
||||
onDone: () => void
|
||||
) {
|
||||
if (!this.isErrorReportingEnabled()) {
|
||||
@@ -313,19 +313,39 @@ class GetErrRequest {
|
||||
}
|
||||
|
||||
const supportsSyntaxGetErr = this.client.apiVersion.gte(API.v440);
|
||||
const allFiles = coalesce(Array.from(files.entries())
|
||||
.filter(entry => supportsSyntaxGetErr || client.hasCapabilityForResource(entry.resource, ClientCapability.Semantic))
|
||||
const fileEntries = Array.from(files.entries()).filter(entry => supportsSyntaxGetErr || client.hasCapabilityForResource(entry.resource, ClientCapability.Semantic));
|
||||
const allFiles = coalesce(fileEntries
|
||||
.map(entry => client.toTsFilePath(entry.resource)));
|
||||
|
||||
if (!allFiles.length) {
|
||||
this._done = true;
|
||||
setImmediate(onDone);
|
||||
} else {
|
||||
const request = this.areProjectDiagnosticsEnabled()
|
||||
let request;
|
||||
if (this.areProjectDiagnosticsEnabled()) {
|
||||
// Note that geterrForProject is almost certainly not the api we want here as it ends up computing far
|
||||
// too many diagnostics
|
||||
? client.executeAsync('geterrForProject', { delay: 0, file: allFiles[0] }, this._token.token)
|
||||
: client.executeAsync('geterr', { delay: 0, files: allFiles }, this._token.token);
|
||||
request = client.executeAsync('geterrForProject', { delay: 0, file: allFiles[0] }, this._token.token);
|
||||
}
|
||||
else {
|
||||
let requestFiles;
|
||||
if (this.areRegionDiagnosticsEnabled()) {
|
||||
requestFiles = coalesce(fileEntries
|
||||
.map(entry => {
|
||||
const file = client.toTsFilePath(entry.resource);
|
||||
const ranges = entry.value;
|
||||
if (file && ranges) {
|
||||
return typeConverters.Range.toFileRangesRequestArgs(file, ranges);
|
||||
}
|
||||
|
||||
return file;
|
||||
}));
|
||||
}
|
||||
else {
|
||||
requestFiles = allFiles;
|
||||
}
|
||||
request = client.executeAsync('geterr', { delay: 0, files: requestFiles }, this._token.token);
|
||||
}
|
||||
|
||||
request.finally(() => {
|
||||
if (this._done) {
|
||||
@@ -350,6 +370,10 @@ class GetErrRequest {
|
||||
return this.client.configuration.enableProjectDiagnostics && this.client.capabilities.has(ClientCapability.Semantic);
|
||||
}
|
||||
|
||||
private areRegionDiagnosticsEnabled() {
|
||||
return this.client.configuration.enableRegionDiagnostics && this.client.apiVersion.gte(API.v560);
|
||||
}
|
||||
|
||||
public cancel(): any {
|
||||
if (!this._done) {
|
||||
this._token.cancel();
|
||||
@@ -722,7 +746,9 @@ export default class BufferSyncSupport extends Disposable {
|
||||
|
||||
// Add all open TS buffers to the geterr request. They might be visible
|
||||
for (const buffer of this.syncedBuffers.values()) {
|
||||
orderedFileSet.set(buffer.resource, undefined);
|
||||
const editors = vscode.window.visibleTextEditors.filter(editor => editor.document.uri.toString() === buffer.resource.toString());
|
||||
const visibleRanges = editors.flatMap(editor => editor.visibleRanges);
|
||||
orderedFileSet.set(buffer.resource, visibleRanges.length ? visibleRanges : undefined);
|
||||
}
|
||||
|
||||
for (const { resource } of orderedFileSet.entries()) {
|
||||
|
||||
@@ -78,6 +78,7 @@ export enum EventName {
|
||||
syntaxDiag = 'syntaxDiag',
|
||||
semanticDiag = 'semanticDiag',
|
||||
suggestionDiag = 'suggestionDiag',
|
||||
regionSemanticDiag = 'regionSemanticDiag',
|
||||
configFileDiag = 'configFileDiag',
|
||||
telemetry = 'telemetry',
|
||||
projectLanguageServiceState = 'projectLanguageServiceState',
|
||||
|
||||
@@ -26,14 +26,24 @@ export namespace Range {
|
||||
Math.max(0, start.line - 1), Math.max(start.offset - 1, 0),
|
||||
Math.max(0, end.line - 1), Math.max(0, end.offset - 1));
|
||||
|
||||
export const toFileRangeRequestArgs = (file: string, range: vscode.Range): Proto.FileRangeRequestArgs => ({
|
||||
file,
|
||||
// @ts-expect-error until ts 5.6
|
||||
export const toFileRange = (range: vscode.Range): Proto.FileRange => ({
|
||||
startLine: range.start.line + 1,
|
||||
startOffset: range.start.character + 1,
|
||||
endLine: range.end.line + 1,
|
||||
endOffset: range.end.character + 1
|
||||
});
|
||||
|
||||
export const toFileRangeRequestArgs = (file: string, range: vscode.Range): Proto.FileRangeRequestArgs => ({
|
||||
file,
|
||||
...toFileRange(range)
|
||||
});
|
||||
// @ts-expect-error until ts 5.6
|
||||
export const toFileRangesRequestArgs = (file: string, ranges: vscode.Range[]): Proto.FileRangesRequestArgs => ({
|
||||
file,
|
||||
ranges: ranges.map(toFileRange)
|
||||
});
|
||||
|
||||
export const toFormattingRequestArgs = (file: string, range: vscode.Range): Proto.FormatRequestArgs => ({
|
||||
file,
|
||||
line: range.start.line + 1,
|
||||
|
||||
@@ -90,8 +90,8 @@ export default class TypeScriptServiceClientHost extends Disposable {
|
||||
services,
|
||||
allModeIds));
|
||||
|
||||
this.client.onDiagnosticsReceived(({ kind, resource, diagnostics }) => {
|
||||
this.diagnosticsReceived(kind, resource, diagnostics);
|
||||
this.client.onDiagnosticsReceived(({ kind, resource, diagnostics, spans }) => {
|
||||
this.diagnosticsReceived(kind, resource, diagnostics, spans);
|
||||
}, null, this._disposables);
|
||||
|
||||
this.client.onConfigDiagnosticsReceived(diag => this.configFileDiagnosticsReceived(diag), null, this._disposables);
|
||||
@@ -236,14 +236,16 @@ export default class TypeScriptServiceClientHost extends Disposable {
|
||||
private async diagnosticsReceived(
|
||||
kind: DiagnosticKind,
|
||||
resource: vscode.Uri,
|
||||
diagnostics: Proto.Diagnostic[]
|
||||
diagnostics: Proto.Diagnostic[],
|
||||
spans: Proto.TextSpan[] | undefined,
|
||||
): Promise<void> {
|
||||
const language = await this.findLanguage(resource);
|
||||
if (language) {
|
||||
language.diagnosticsReceived(
|
||||
kind,
|
||||
resource,
|
||||
this.createMarkerDatas(diagnostics, language.diagnosticSource));
|
||||
this.createMarkerDatas(diagnostics, language.diagnosticSource),
|
||||
spans?.map(span => typeConverters.Range.fromTextSpan(span)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface TsDiagnostics {
|
||||
readonly kind: DiagnosticKind;
|
||||
readonly resource: vscode.Uri;
|
||||
readonly diagnostics: Proto.Diagnostic[];
|
||||
readonly spans?: Proto.TextSpan[];
|
||||
}
|
||||
|
||||
interface ToCancelOnResourceChanged {
|
||||
@@ -947,7 +948,8 @@ export default class TypeScriptServiceClient extends Disposable implements IType
|
||||
switch (event.event) {
|
||||
case EventName.syntaxDiag:
|
||||
case EventName.semanticDiag:
|
||||
case EventName.suggestionDiag: {
|
||||
case EventName.suggestionDiag:
|
||||
case EventName.regionSemanticDiag: {
|
||||
// This event also roughly signals that projects have been loaded successfully (since the TS server is synchronous)
|
||||
this.loadingIndicator.reset();
|
||||
|
||||
@@ -956,7 +958,9 @@ export default class TypeScriptServiceClient extends Disposable implements IType
|
||||
this._onDiagnosticsReceived.fire({
|
||||
kind: getDiagnosticsKind(event),
|
||||
resource: this.toResource(diagnosticEvent.body.file),
|
||||
diagnostics: diagnosticEvent.body.diagnostics
|
||||
diagnostics: diagnosticEvent.body.diagnostics,
|
||||
// @ts-expect-error until ts 5.6
|
||||
spans: diagnosticEvent.body.spans,
|
||||
});
|
||||
}
|
||||
break;
|
||||
@@ -1261,6 +1265,7 @@ function getDiagnosticsKind(event: Proto.Event) {
|
||||
case 'syntaxDiag': return DiagnosticKind.Syntax;
|
||||
case 'semanticDiag': return DiagnosticKind.Semantic;
|
||||
case 'suggestionDiag': return DiagnosticKind.Suggestion;
|
||||
case 'regionSemanticDiag': return DiagnosticKind.RegionSemantic;
|
||||
}
|
||||
throw new Error('Unknown dignostics kind');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user