Skip to content

Commit

Permalink
use language indicator for folding limit warnings. For #142496
Browse files Browse the repository at this point in the history
  • Loading branch information
aeschli committed Jun 24, 2022
1 parent 716fe5e commit 06950eb
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 78 deletions.
77 changes: 38 additions & 39 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,
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, ProvideFoldingRangeSignature, ProvideDocumentSymbolsSignature
DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, ProvideHoverSignature, BaseLanguageClient, ProvideFoldingRangeSignature, ProvideDocumentSymbolsSignature, ProvideDocumentColorsSignature
} from 'vscode-languageclient';


import { hash } from './utils/hash';
import { createDocumentSymbolsLimitItem, createFoldingRangeLimitItem, createLanguageStatusItem, createLimitStatusItem } 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 Down Expand Up @@ -129,9 +121,10 @@ export async function startClient(context: ExtensionContext, newLanguageClient:

let isClientReady = false;

const foldingRangeLimitStatusBarItem = createLimitStatusItem(documentSelector, createFoldingRangeLimitItem);
const documentSymbolsLimitStatusbarItem = createLimitStatusItem(documentSelector, createDocumentSymbolsLimitItem);
toDispose.push(foldingRangeLimitStatusBarItem, documentSymbolsLimitStatusbarItem);
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) {
Expand Down Expand Up @@ -221,10 +214,10 @@ export async function startClient(context: ExtensionContext, newLanguageClient:
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;
foldingRangeLimitStatusBarItem.show(resultLimit);
} else if (foldingRangeLimitStatusBarItem) {
foldingRangeLimitStatusBarItem.hide();
r.length = resultLimit; // truncate
foldingRangeLimitStatusBarItem.update(document, resultLimit);
} else {
foldingRangeLimitStatusBarItem.update(document, false);
}
return r;
}
Expand All @@ -234,13 +227,35 @@ export async function startClient(context: ExtensionContext, newLanguageClient:
}
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) && r.length > resultLimit) {
documentSymbolsLimitStatusbarItem.show(resultLimit);
} else if (foldingRangeLimitStatusBarItem) {
documentSymbolsLimitStatusbarItem.hide();
if (Array.isArray(r) && (isDocumentSymbol(r) ? countDocumentSymbols(r) : r.length) > resultLimit) {
documentSymbolsLimitStatusbarItem.update(document, resultLimit);
} else {
documentSymbolsLimitStatusbarItem.update(document, false);
}
return r;
}
Expand Down Expand Up @@ -366,22 +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
103 changes: 74 additions & 29 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, LanguageStatusItem } 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,44 +221,85 @@ export function createLanguageStatusItem(documentSelector: string[], statusReque
return Disposable.from(statusItem, activeEditorListener, showSchemasCommand);
}

export function createLimitStatusItem(documentSelector: string[], newItem: (documentSelector: string[], limit: number) => Disposable) {
export function createLimitStatusItem(newItem: (limit: number) => Disposable) {
let statusItem: Disposable | undefined;
let lastLimit: number = -1;
return {
show(limit: number) {
if (!statusItem || limit !== lastLimit) {
statusItem?.dispose();
statusItem = newItem(documentSelector, limit);
lastLimit = limit;
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);
}
},
hide() {
statusItem?.dispose();
statusItem = undefined;
},
}
}));
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[], limit: number): Disposable {
const statusItem = languages.createLanguageStatusItem('json.foldingStatus', documentSelector);
statusItem.name = localize('foldingStatusItem.name', "JSON Folding Status");
statusItem.severity = LanguageStatusSeverity.Information;
statusItem.text = localize('status.limitedRanges.short', "Folding Ranges Limited");
statusItem.detail = localize('status.limitedRanges.details', 'Only {0} folding ranges shown.', limit);
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[], limit: number): Disposable {
const statusItem = languages.createLanguageStatusItem('json.documentSymbolStatus', documentSelector);
statusItem.name = localize('documentSymbolStatusItem.name', "JSON Document Symbol Status");
statusItem.severity = LanguageStatusSeverity.Information;
statusItem.text = localize('status.limitedDocumentSymbol.short', "Document Symbol Limited");
statusItem.detail = localize('status.limitedDocumentSymbol.details', 'Only {0} document symbols shown.', limit);
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
4 changes: 0 additions & 4 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

0 comments on commit 06950eb

Please sign in to comment.