Skip to content

Commit

Permalink
[json] use language indicator for folding limit warnings (#153104)
Browse files Browse the repository at this point in the history
* reduce number of json notifications

* use language indicator for folding limit warnings. For #142496

* add comment for resultLimit + 1
  • Loading branch information
aeschli committed Jun 24, 2022
1 parent bdfb4d0 commit b3cc787
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 90 deletions.
100 changes: 69 additions & 31 deletions extensions/json-language-features/client/src/jsonClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@ const localize = nls.loadMessageBundle();
export type JSONLanguageStatus = { schemas: string[] };

import {
workspace, window, languages, commands, ExtensionContext, extensions, Uri,
Diagnostic, StatusBarAlignment, TextEditor, TextDocument, FormattingOptions, CancellationToken,
ProviderResult, TextEdit, Range, Position, Disposable, CompletionItem, CompletionList, CompletionContext, Hover, MarkdownString,
workspace, window, languages, commands, ExtensionContext, extensions, Uri, ColorInformation,
Diagnostic, StatusBarAlignment, TextEditor, TextDocument, FormattingOptions, CancellationToken, FoldingRange,
ProviderResult, TextEdit, Range, Position, Disposable, CompletionItem, CompletionList, CompletionContext, Hover, MarkdownString, FoldingContext, DocumentSymbol, SymbolInformation
} from 'vscode';
import {
LanguageClientOptions, RequestType, NotificationType,
DidChangeConfigurationNotification, HandleDiagnosticsSignature, ResponseError, DocumentRangeFormattingParams,
DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, ProvideHoverSignature, BaseLanguageClient
DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, ProvideHoverSignature, BaseLanguageClient, ProvideFoldingRangeSignature, ProvideDocumentSymbolsSignature, ProvideDocumentColorsSignature
} from 'vscode-languageclient';


import { hash } from './utils/hash';
import { createLanguageStatusItem } from './languageStatus';
import { createDocumentColorsLimitItem, createDocumentSymbolsLimitItem, createFoldingRangeLimitItem, createLanguageStatusItem, createLimitStatusItem } from './languageStatus';

namespace VSCodeContentRequest {
export const type: RequestType<string, string, any> = new RequestType('vscode/content');
Expand Down Expand Up @@ -53,10 +53,6 @@ namespace SchemaAssociationNotification {
export const type: NotificationType<ISchemaAssociations | ISchemaAssociation[]> = new NotificationType('json/schemaAssociations');
}

namespace ResultLimitReachedNotification {
export const type: NotificationType<string> = new NotificationType('json/resultLimitReached');
}

type Settings = {
json?: {
schemas?: JSONSchemaSettings[];
Expand All @@ -76,17 +72,13 @@ export type JSONSchemaSettings = {
schema?: any;
};

namespace SettingIds {
export namespace SettingIds {
export const enableFormatter = 'json.format.enable';
export const enableValidation = 'json.validate.enable';
export const enableSchemaDownload = 'json.schemaDownload.enable';
export const maxItemsComputed = 'json.maxItemsComputed';
}

namespace StorageIds {
export const maxItemsExceededInformation = 'json.maxItemsExceededInformation';
}

export interface TelemetryReporter {
sendTelemetryEvent(eventName: string, properties?: {
[key: string]: string;
Expand All @@ -109,6 +101,8 @@ export interface SchemaRequestService {

export const languageServerDescription = localize('jsonserver.name', 'JSON Language Server');

let resultLimit = 5000;

export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise<BaseLanguageClient> {

const toDispose = context.subscriptions;
Expand All @@ -127,6 +121,11 @@ export async function startClient(context: ExtensionContext, newLanguageClient:

let isClientReady = false;

const foldingRangeLimitStatusBarItem = createLimitStatusItem((limit: number) => createFoldingRangeLimitItem(documentSelector, SettingIds.maxItemsComputed, limit));
const documentSymbolsLimitStatusbarItem = createLimitStatusItem((limit: number) => createDocumentSymbolsLimitItem(documentSelector, SettingIds.maxItemsComputed, limit));
const documentColorsLimitStatusbarItem = createLimitStatusItem((limit: number) => createDocumentColorsLimitItem(documentSelector, SettingIds.maxItemsComputed, limit));
toDispose.push(foldingRangeLimitStatusBarItem, documentSymbolsLimitStatusbarItem, documentColorsLimitStatusbarItem);

toDispose.push(commands.registerCommand('json.clearCache', async () => {
if (isClientReady && runtime.schemaRequests.clearCache) {
const cachedSchemas = await runtime.schemaRequests.clearCache();
Expand Down Expand Up @@ -211,6 +210,60 @@ export async function startClient(context: ExtensionContext, newLanguageClient:
return r.then(updateHover);
}
return updateHover(r);
},
provideFoldingRanges(document: TextDocument, context: FoldingContext, token: CancellationToken, next: ProvideFoldingRangeSignature) {
function checkLimit(r: FoldingRange[] | null | undefined): FoldingRange[] | null | undefined {
if (Array.isArray(r) && r.length > resultLimit) {
r.length = resultLimit; // truncate
foldingRangeLimitStatusBarItem.update(document, resultLimit);
} else {
foldingRangeLimitStatusBarItem.update(document, false);
}
return r;
}
const r = next(document, context, token);
if (isThenable<FoldingRange[] | null | undefined>(r)) {
return r.then(checkLimit);
}
return checkLimit(r);
},
provideDocumentColors(document: TextDocument, token: CancellationToken, next: ProvideDocumentColorsSignature) {
function checkLimit(r: ColorInformation[] | null | undefined): ColorInformation[] | null | undefined {
if (Array.isArray(r) && r.length > resultLimit) {
r.length = resultLimit; // truncate
documentColorsLimitStatusbarItem.update(document, resultLimit);
} else {
documentColorsLimitStatusbarItem.update(document, false);
}
return r;
}
const r = next(document, token);
if (isThenable<ColorInformation[] | null | undefined>(r)) {
return r.then(checkLimit);
}
return checkLimit(r);
},
provideDocumentSymbols(document: TextDocument, token: CancellationToken, next: ProvideDocumentSymbolsSignature) {
type T = SymbolInformation[] | DocumentSymbol[];
function countDocumentSymbols(symbols: DocumentSymbol[]): number {
return symbols.reduce((previousValue, s) => previousValue + 1 + countDocumentSymbols(s.children), 0);
}
function isDocumentSymbol(r: T): r is DocumentSymbol[] {
return r[0] instanceof DocumentSymbol;
}
function checkLimit(r: T | null | undefined): T | null | undefined {
if (Array.isArray(r) && (isDocumentSymbol(r) ? countDocumentSymbols(r) : r.length) > resultLimit) {
documentSymbolsLimitStatusbarItem.update(document, resultLimit);
} else {
documentSymbolsLimitStatusbarItem.update(document, false);
}
return r;
}
const r = next(document, token);
if (isThenable<T | undefined | null>(r)) {
return r.then(checkLimit);
}
return checkLimit(r);
}
}
};
Expand Down Expand Up @@ -328,21 +381,6 @@ export async function startClient(context: ExtensionContext, newLanguageClient:
}
}));

client.onNotification(ResultLimitReachedNotification.type, async message => {
const shouldPrompt = context.globalState.get<boolean>(StorageIds.maxItemsExceededInformation) !== false;
if (shouldPrompt) {
const ok = localize('ok', "OK");
const openSettings = localize('goToSetting', 'Open Settings');
const neverAgain = localize('yes never again', "Don't Show Again");
const pick = await window.showInformationMessage(`${message}\n${localize('configureLimit', 'Use setting \'{0}\' to configure the limit.', SettingIds.maxItemsComputed)}`, ok, openSettings, neverAgain);
if (pick === neverAgain) {
await context.globalState.update(StorageIds.maxItemsExceededInformation, false);
} else if (pick === openSettings) {
await commands.executeCommand('workbench.action.openSettings', SettingIds.maxItemsComputed);
}
}
});

toDispose.push(createLanguageStatusItem(documentSelector, (uri: string) => client.sendRequest(LanguageStatusRequest.type, uri)));

function updateFormatterRegistration() {
Expand Down Expand Up @@ -432,7 +470,7 @@ function getSettings(): Settings {
const configuration = workspace.getConfiguration();
const httpSettings = workspace.getConfiguration('http');

const resultLimit: number = Math.trunc(Math.max(0, Number(workspace.getConfiguration().get(SettingIds.maxItemsComputed)))) || 5000;
resultLimit = Math.trunc(Math.max(0, Number(workspace.getConfiguration().get(SettingIds.maxItemsComputed)))) || 5000;

const settings: Settings = {
http: {
Expand All @@ -443,7 +481,7 @@ function getSettings(): Settings {
validate: { enable: configuration.get(SettingIds.enableValidation) },
format: { enable: configuration.get(SettingIds.enableFormatter) },
schemas: [],
resultLimit
resultLimit: resultLimit + 1 // ask for one more so we can detect if the limit has been exceeded
}
};
const schemaSettingsById: { [schemaId: string]: JSONSchemaSettings } = Object.create(null);
Expand Down
94 changes: 90 additions & 4 deletions extensions/json-language-features/client/src/languageStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { window, languages, Uri, LanguageStatusSeverity, Disposable, commands, QuickPickItem, extensions, workspace, Extension, WorkspaceFolder, QuickPickItemKind, ThemeIcon } from 'vscode';
import {
window, languages, Uri, Disposable, commands, QuickPickItem,
extensions, workspace, Extension, WorkspaceFolder, QuickPickItemKind,
ThemeIcon, TextDocument, LanguageStatusSeverity
} from 'vscode';
import { JSONLanguageStatus, JSONSchemaSettings } from './jsonClient';

import * as nls from 'vscode-nls';
Expand Down Expand Up @@ -187,13 +191,13 @@ export function createLanguageStatusItem(documentSelector: string[], statusReque
statusItem.detail = undefined;
if (schemas.length === 0) {
statusItem.text = localize('status.noSchema.short', "No Schema Validation");
statusItem.detail = localize('status.noSchema', 'No JSON schema configured.');
statusItem.detail = localize('status.noSchema', 'no JSON schema configured');
} else if (schemas.length === 1) {
statusItem.text = localize('status.withSchema.short', "Schema Validated");
statusItem.detail = localize('status.singleSchema', 'JSON schema configured.');
statusItem.detail = localize('status.singleSchema', 'JSON schema configured');
} else {
statusItem.text = localize('status.withSchemas.short', "Schema Validated");
statusItem.detail = localize('status.multipleSchema', 'Multiple JSON schemas configured.');
statusItem.detail = localize('status.multipleSchema', 'multiple JSON schemas configured');
}
statusItem.command = {
command: '_json.showAssociatedSchemaList',
Expand All @@ -217,3 +221,85 @@ export function createLanguageStatusItem(documentSelector: string[], statusReque
return Disposable.from(statusItem, activeEditorListener, showSchemasCommand);
}

export function createLimitStatusItem(newItem: (limit: number) => Disposable) {
let statusItem: Disposable | undefined;
const activeLimits: Map<TextDocument, number> = new Map();

const toDispose: Disposable[] = [];
toDispose.push(window.onDidChangeActiveTextEditor(textEditor => {
statusItem?.dispose();
statusItem = undefined;
const doc = textEditor?.document;
if (doc) {
const limit = activeLimits.get(doc);
if (limit !== undefined) {
statusItem = newItem(limit);
}
}
}));
toDispose.push(workspace.onDidCloseTextDocument(document => {
activeLimits.delete(document);
}));

function update(document: TextDocument, limitApplied: number | false) {
if (limitApplied === false) {
activeLimits.delete(document);
if (statusItem && document === window.activeTextEditor?.document) {
statusItem.dispose();
statusItem = undefined;
}
} else {
activeLimits.set(document, limitApplied);
if (document === window.activeTextEditor?.document) {
if (!statusItem || limitApplied !== activeLimits.get(document)) {
statusItem?.dispose();
statusItem = newItem(limitApplied);
}
}
}
}
return {
update,
dispose() {
statusItem?.dispose();
toDispose.forEach(d => d.dispose());
toDispose.length = 0;
statusItem = undefined;
activeLimits.clear();
}
};
}

const openSettingsCommand = 'workbench.action.openSettings';
const configureSettingsLabel = localize('status.button.configure', "Configure");

export function createFoldingRangeLimitItem(documentSelector: string[], settingId: string, limit: number): Disposable {
const statusItem = languages.createLanguageStatusItem('json.foldingRangesStatus', documentSelector);
statusItem.name = localize('foldingRangesStatusItem.name', "JSON Folding Status");
statusItem.severity = LanguageStatusSeverity.Warning;
statusItem.text = localize('status.limitedFoldingRanges.short', "Folding Ranges Limited");
statusItem.detail = localize('status.limitedFoldingRanges.details', 'only {0} folding ranges shown', limit);
statusItem.command = { command: openSettingsCommand, arguments: [settingId], title: configureSettingsLabel };
return Disposable.from(statusItem);
}

export function createDocumentSymbolsLimitItem(documentSelector: string[], settingId: string, limit: number): Disposable {
const statusItem = languages.createLanguageStatusItem('json.documentSymbolsStatus', documentSelector);
statusItem.name = localize('documentSymbolsStatusItem.name', "JSON Outline Status");
statusItem.severity = LanguageStatusSeverity.Warning;
statusItem.text = localize('status.limitedDocumentSymbols.short', "Outline Limited");
statusItem.detail = localize('status.limitedDocumentSymbols.details', 'only {0} document symbols shown', limit);
statusItem.command = { command: openSettingsCommand, arguments: [settingId], title: configureSettingsLabel };
return Disposable.from(statusItem);
}

export function createDocumentColorsLimitItem(documentSelector: string[], settingId: string, limit: number): Disposable {
const statusItem = languages.createLanguageStatusItem('json.documentColorsStatus', documentSelector);
statusItem.name = localize('documentColorsStatusItem.name', "JSON Color Symbol Status");
statusItem.severity = LanguageStatusSeverity.Warning;
statusItem.text = localize('status.limitedDocumentColors.short', "Color Symbols Limited");
statusItem.detail = localize('status.limitedDocumentColors.details', 'only {0} color decorators shown', limit);
statusItem.command = { command: openSettingsCommand, arguments: [settingId], title: configureSettingsLabel };
return Disposable.from(statusItem);
}

6 changes: 0 additions & 6 deletions extensions/json-language-features/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,6 @@ Notification:
### Item Limit

If the setting `resultLimit` is set, the JSON language server will limit the number of folding ranges and document symbols computed.
When the limit is reached, a notification `json/resultLimitReached` is sent that can be shown that can be shown to the user.

Notification:
- method: 'json/resultLimitReached'
- params: a human readable string to show to the user.


## Try

Expand Down
54 changes: 5 additions & 49 deletions extensions/json-language-features/server/src/jsonServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,6 @@ namespace SchemaContentChangeNotification {
export const type: NotificationType<string | string[]> = new NotificationType('json/schemaContent');
}

namespace ResultLimitReachedNotification {
export const type: NotificationType<string> = new NotificationType('json/resultLimitReached');
}

namespace ForceValidateRequest {
export const type: RequestType<string, Diagnostic[], any> = new RequestType('json/validate');
}
Expand Down Expand Up @@ -204,44 +200,6 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
}


const limitExceededWarnings = function () {
const pendingWarnings: { [uri: string]: { features: { [name: string]: string }; timeout?: Disposable } } = {};

const showLimitedNotification = (uri: string, resultLimit: number) => {
const warning = pendingWarnings[uri];
connection.sendNotification(ResultLimitReachedNotification.type, `${Utils.basename(URI.parse(uri))}: For performance reasons, ${Object.keys(warning.features).join(' and ')} have been limited to ${resultLimit} items.`);
warning.timeout = undefined;
};

return {
cancel(uri: string) {
const warning = pendingWarnings[uri];
if (warning && warning.timeout) {
warning.timeout.dispose();
delete pendingWarnings[uri];
}
},

onResultLimitExceeded(uri: string, resultLimit: number, name: string) {
return () => {
let warning = pendingWarnings[uri];
if (warning) {
if (!warning.timeout) {
// already shown
return;
}
warning.features[name] = name;
warning.timeout.dispose();
warning.timeout = runtime.timer.setTimeout(() => showLimitedNotification(uri, resultLimit), 2000);
} else {
warning = { features: { [name]: name } };
warning.timeout = runtime.timer.setTimeout(() => showLimitedNotification(uri, resultLimit), 2000);
pendingWarnings[uri] = warning;
}
};
}
};
}();

let jsonConfigurationSettings: JSONSchemaSettings[] | undefined = undefined;
let schemaAssociations: ISchemaAssociations | SchemaConfiguration[] | undefined = undefined;
Expand Down Expand Up @@ -417,11 +375,10 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
const document = documents.get(documentSymbolParams.textDocument.uri);
if (document) {
const jsonDocument = getJSONDocument(document);
const onResultLimitExceeded = limitExceededWarnings.onResultLimitExceeded(document.uri, resultLimit, 'document symbols');
if (hierarchicalDocumentSymbolSupport) {
return languageService.findDocumentSymbols2(document, jsonDocument, { resultLimit, onResultLimitExceeded });
return languageService.findDocumentSymbols2(document, jsonDocument, { resultLimit });
} else {
return languageService.findDocumentSymbols(document, jsonDocument, { resultLimit, onResultLimitExceeded });
return languageService.findDocumentSymbols(document, jsonDocument, { resultLimit });
}
}
return [];
Expand Down Expand Up @@ -453,9 +410,9 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
return runSafeAsync(runtime, async () => {
const document = documents.get(params.textDocument.uri);
if (document) {
const onResultLimitExceeded = limitExceededWarnings.onResultLimitExceeded(document.uri, resultLimit, 'document colors');

const jsonDocument = getJSONDocument(document);
return languageService.findDocumentColors(document, jsonDocument, { resultLimit, onResultLimitExceeded });
return languageService.findDocumentColors(document, jsonDocument, { resultLimit });
}
return [];
}, [], `Error while computing document colors for ${params.textDocument.uri}`, token);
Expand All @@ -476,8 +433,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
return runSafe(runtime, () => {
const document = documents.get(params.textDocument.uri);
if (document) {
const onRangeLimitExceeded = limitExceededWarnings.onResultLimitExceeded(document.uri, foldingRangeLimit, 'folding ranges');
return languageService.getFoldingRanges(document, { rangeLimit: foldingRangeLimit, onRangeLimitExceeded });
return languageService.getFoldingRanges(document, { rangeLimit: foldingRangeLimit });
}
return null;
}, null, `Error while computing folding ranges for ${params.textDocument.uri}`, token);
Expand Down

0 comments on commit b3cc787

Please sign in to comment.