From 5872a8e368c1ff5b9519651abb05ed09c39bb52c Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Thu, 13 Aug 2026 14:50:28 +0200 Subject: [PATCH] fix(json): make the JSON server's computation limit configurable The VSCode JSON language server computes document symbols and folding ranges without limit unless the client configures one. Wild Web Developer never sent any configuration, so a 100,000 line file produced 20,001 symbols and folding ranges per request, and with the Outline view open the UI thread spent seconds per keystroke rehashing that tree. Add a JSON preference page with the maximum number of computed items, defaulting to 5000 as VSCode and the existing YAML preference do, and 0 for no limit. The validate and http entries have to be sent along because the server re-reads every setting from each notification: without validate.enable it disables validation, and without http.proxyStrictSSL it stops verifying certificates when downloading remote schemas. --- .../tests/TestJSONResultLimitPreference.java | 113 ++++++++++++++++++ .../META-INF/MANIFEST.MF | 4 +- .../plugin.properties | 2 + org.eclipse.wildwebdeveloper/plugin.xml | 14 +++ .../json/JSonLanguageServer.java | 19 ++- .../wildwebdeveloper/json/ui/Messages.java | 28 +++++ .../json/ui/messages.properties | 14 +++ .../JSonPreferenceInitializer.java | 27 +++++ .../ui/preferences/JSonPreferencePage.java | 46 +++++++ .../JSonPreferenceServerConstants.java | 71 +++++++++++ 10 files changed, 334 insertions(+), 4 deletions(-) create mode 100644 org.eclipse.wildwebdeveloper.tests/src/org/eclipse/wildwebdeveloper/tests/TestJSONResultLimitPreference.java create mode 100644 org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/json/ui/Messages.java create mode 100644 org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/json/ui/messages.properties create mode 100644 org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/json/ui/preferences/JSonPreferenceInitializer.java create mode 100644 org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/json/ui/preferences/JSonPreferencePage.java create mode 100644 org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/json/ui/preferences/JSonPreferenceServerConstants.java diff --git a/org.eclipse.wildwebdeveloper.tests/src/org/eclipse/wildwebdeveloper/tests/TestJSONResultLimitPreference.java b/org.eclipse.wildwebdeveloper.tests/src/org/eclipse/wildwebdeveloper/tests/TestJSONResultLimitPreference.java new file mode 100644 index 0000000000..21e1b18887 --- /dev/null +++ b/org.eclipse.wildwebdeveloper.tests/src/org/eclipse/wildwebdeveloper/tests/TestJSONResultLimitPreference.java @@ -0,0 +1,113 @@ +/******************************************************************************* + * Copyright (c) 2026 Vogella GmbH and others. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Lars Vogel (Vogella GmbH) - initial implementation + *******************************************************************************/ +package org.eclipse.wildwebdeveloper.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.jface.text.IDocument; +import org.eclipse.lsp4e.LSPEclipseUtils; +import org.eclipse.lsp4e.LanguageServers; +import org.eclipse.lsp4e.LanguageServersRegistry; +import org.eclipse.lsp4j.DocumentSymbolParams; +import org.eclipse.lsp4j.ServerCapabilities; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.services.LanguageServer; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.ide.IDE; +import org.eclipse.ui.tests.harness.util.DisplayHelper; +import org.eclipse.ui.texteditor.AbstractTextEditor; +import org.eclipse.wildwebdeveloper.Activator; +import org.eclipse.wildwebdeveloper.json.JSonLanguageServer; +import org.eclipse.wildwebdeveloper.json.ui.preferences.JSonPreferenceServerConstants; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(AllCleanRule.class) +class TestJSONResultLimitPreference { + + private static final int KEYS = 60; + private static final int REDUCED_LIMIT = 5; + + private IProject project; + + @BeforeEach + void setUpProject() throws Exception { + this.project = ResourcesPlugin.getWorkspace().getRoot().getProject(getClass().getName() + System.nanoTime()); + project.create(null); + project.open(null); + } + + @AfterEach + void resetPreference() { + Activator.getDefault().getPreferenceStore().setValue( + JSonPreferenceServerConstants.JSON_PREFERENCES_MAXITEMSCOMPUTED, + JSonPreferenceServerConstants.MAXITEMSCOMPUTED_DEFAULT); + } + + @Test + void testResultLimitPreferenceAppliesWithoutRestart() throws Exception { + StringBuilder content = new StringBuilder("{\n"); + for (int i = 0; i < KEYS; i++) { + content.append(" \"key_").append(i).append("\": ").append(i).append(i == KEYS - 1 ? "\n" : ",\n"); + } + content.append("}\n"); + + IFile file = project.getFile("limit.json"); + file.create(content.toString().getBytes(StandardCharsets.UTF_8), true, false, null); + + AbstractTextEditor editor = (AbstractTextEditor) IDE.openEditor( + PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(), file, + "org.eclipse.ui.genericeditor.GenericEditor"); + IDocument document = LSPEclipseUtils.getDocument(editor); + DisplayHelper.sleep(2000); + + assertEquals(KEYS, countSymbols(document), "Default limit should not truncate this document"); + + Activator.getDefault().getPreferenceStore() + .setValue(JSonPreferenceServerConstants.JSON_PREFERENCES_MAXITEMSCOMPUTED, REDUCED_LIMIT); + DisplayHelper.sleep(1500); // allow the asynchronous preference broadcast + + // exact, so that an empty result does not silently satisfy the assertion + assertEquals(REDUCED_LIMIT, countSymbols(document), + "Lowering the limit should truncate the outline to exactly that many symbols"); + } + + private static int countSymbols(IDocument document) throws Exception { + DocumentSymbolParams params = new DocumentSymbolParams( + new TextDocumentIdentifier(LSPEclipseUtils.toUri(document).toString())); + return jsonLanguageServer(document).getTextDocumentService().documentSymbol(params).get(10, TimeUnit.SECONDS) + .size(); + } + + private static LanguageServer jsonLanguageServer(IDocument document) throws Exception { + return LanguageServers.forDocument(document) // + .withCapability(ServerCapabilities::getDocumentSymbolProvider) // + .collectAll((wrapper, ls) -> CompletableFuture.completedFuture( // + LanguageServersRegistry.getInstance() + .getDefinition(JSonLanguageServer.JSON_LANGUAGE_SERVER_ID) + .equals(wrapper.serverDefinition) ? ls : null)) // + .get(10, TimeUnit.SECONDS).stream() // + .filter(Objects::nonNull) // + .findFirst() // + .orElseThrow(() -> new AssertionError("Expected a running JSON language server for the document")); + } +} diff --git a/org.eclipse.wildwebdeveloper/META-INF/MANIFEST.MF b/org.eclipse.wildwebdeveloper/META-INF/MANIFEST.MF index 244554cef4..dbaf9f80c9 100644 --- a/org.eclipse.wildwebdeveloper/META-INF/MANIFEST.MF +++ b/org.eclipse.wildwebdeveloper/META-INF/MANIFEST.MF @@ -41,4 +41,6 @@ Export-Package: org.eclipse.wildwebdeveloper;x-friends:="org.eclipse.wildwebdeve org.eclipse.wildwebdeveloper.debug.npm;x-internal:=true, org.eclipse.wildwebdeveloper.markdown;x-friends:="org.eclipse.wildwebdeveloper.tests", org.eclipse.wildwebdeveloper.jsts;x-friends:="org.eclipse.wildwebdeveloper.tests", - org.eclipse.wildwebdeveloper.jsts.ui.preferences;x-friends:="org.eclipse.wildwebdeveloper.tests" + org.eclipse.wildwebdeveloper.jsts.ui.preferences;x-friends:="org.eclipse.wildwebdeveloper.tests", + org.eclipse.wildwebdeveloper.json;x-friends:="org.eclipse.wildwebdeveloper.tests", + org.eclipse.wildwebdeveloper.json.ui.preferences;x-friends:="org.eclipse.wildwebdeveloper.tests" diff --git a/org.eclipse.wildwebdeveloper/plugin.properties b/org.eclipse.wildwebdeveloper/plugin.properties index 2cfd9b95dd..178822972e 100644 --- a/org.eclipse.wildwebdeveloper/plugin.properties +++ b/org.eclipse.wildwebdeveloper/plugin.properties @@ -48,6 +48,7 @@ JSTSFormatterPreferencePage.name=Formatter # Markdown MarkdownPreferencePage.name=Markdown (Wild Web Developer) +JSonPreferencePage.name=JSON (Wild Web Developer) MarkdownProblem=Markdown Problem # YAML @@ -69,3 +70,4 @@ preferenceKeywords.scss=scss preferenceKeywords.sass=sass preferenceKeywords.html=html preferenceKeywords.markdown=markdown +preferenceKeywords.json=json diff --git a/org.eclipse.wildwebdeveloper/plugin.xml b/org.eclipse.wildwebdeveloper/plugin.xml index 9ad200e3bf..99ee8c9c08 100644 --- a/org.eclipse.wildwebdeveloper/plugin.xml +++ b/org.eclipse.wildwebdeveloper/plugin.xml @@ -131,6 +131,19 @@ + + + + + + + + + + diff --git a/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/json/JSonLanguageServer.java b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/json/JSonLanguageServer.java index f27457de92..36279a0ce2 100644 --- a/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/json/JSonLanguageServer.java +++ b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/json/JSonLanguageServer.java @@ -37,7 +37,7 @@ import org.eclipse.lsp4e.LanguageServers; import org.eclipse.lsp4e.LanguageServersRegistry; import org.eclipse.lsp4e.LanguageServersRegistry.LanguageServerDefinition; -import org.eclipse.lsp4e.server.ProcessStreamConnectionProvider; +import org.eclipse.lsp4j.DidChangeConfigurationParams; import org.eclipse.lsp4j.InitializeResult; import org.eclipse.lsp4j.jsonrpc.messages.Message; import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage; @@ -46,20 +46,26 @@ import org.eclipse.wildwebdeveloper.SchemaAssociationRegistry; import org.eclipse.wildwebdeveloper.SchemaAssociationsPreferenceInitializer; import org.eclipse.wildwebdeveloper.embedder.node.NodeJSManager; +import org.eclipse.wildwebdeveloper.json.ui.preferences.JSonPreferenceServerConstants; +import org.eclipse.wildwebdeveloper.ui.preferences.ProcessStreamConnectionProviderWithPreference; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; @SuppressWarnings("restriction") -public class JSonLanguageServer extends ProcessStreamConnectionProvider { +public class JSonLanguageServer extends ProcessStreamConnectionProviderWithPreference { public final static String SCHEMA_EXT = "org.eclipse.wildwebdeveloper.json.schema"; //$NON-NLS-1$ public final static String PATTERN_ATTR = "pattern"; //$NON-NLS-1$ public final static String URL_ATTR = "url"; //$NON-NLS-1$ + public static final String JSON_LANGUAGE_SERVER_ID = "org.eclipse.wildwebdeveloper.json"; //$NON-NLS-1$ + + private static final String[] SUPPORTED_SECTIONS = { "json" }; //$NON-NLS-1$ + private static final IPreferenceStore PREFERENCE_STORE = Activator.getDefault().getPreferenceStore(); private static final LanguageServerDefinition JSON_LS_DEFINITION = LanguageServersRegistry.getInstance() - .getDefinition("org.eclipse.wildwebdeveloper.json"); + .getDefinition(JSON_LANGUAGE_SERVER_ID); private static final IPropertyChangeListener PROPERTY_CHANGE_LISTENER = new IPropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent event) { @@ -74,6 +80,7 @@ public void propertyChange(PropertyChangeEvent event) { }; public JSonLanguageServer() { + super(JSON_LANGUAGE_SERVER_ID, PREFERENCE_STORE, SUPPORTED_SECTIONS); List commands = new ArrayList<>(); commands.add(NodeJSManager.getNodeJsLocation().getAbsolutePath()); try { @@ -96,10 +103,16 @@ public void handleMessage(Message message, LanguageServer languageServer, URI ro // Language server side. JSonLanguageServerInterface server = (JSonLanguageServerInterface) languageServer; server.sendJSonchemaAssociations(getSchemaAssociations()); + server.getWorkspaceService().didChangeConfiguration(new DidChangeConfigurationParams(createSettings())); } } } + @Override + protected Object createSettings() { + return JSonPreferenceServerConstants.getGlobalSettings(); + } + private static Map> getSchemaAssociations() { Map> associations = new HashMap<>(); fillSchemaAssociationsFromPreferenceStore(associations); diff --git a/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/json/ui/Messages.java b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/json/ui/Messages.java new file mode 100644 index 0000000000..ad31f11c5f --- /dev/null +++ b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/json/ui/Messages.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2026 Vogella GmbH and others. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Lars Vogel (Vogella GmbH) - initial implementation + *******************************************************************************/ +package org.eclipse.wildwebdeveloper.json.ui; + +import org.eclipse.osgi.util.NLS; + +/** + * JSON messages keys. + * + */ +public class Messages extends NLS { + + // --------- JSON Main preference page + public static String JSonPreferencePage_maxItemsComputed; + + static { + NLS.initializeMessages("org.eclipse.wildwebdeveloper.json.ui.messages", Messages.class); //$NON-NLS-1$ + } +} diff --git a/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/json/ui/messages.properties b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/json/ui/messages.properties new file mode 100644 index 0000000000..0a1aabfc46 --- /dev/null +++ b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/json/ui/messages.properties @@ -0,0 +1,14 @@ +#/******************************************************************************* +# * Copyright (c) 2026 Vogella GmbH and others. +# * This program and the accompanying materials are made +# * available under the terms of the Eclipse Public License 2.0 +# * which is available at https://www.eclipse.org/legal/epl-2.0/ +# * +# * SPDX-License-Identifier: EPL-2.0 +# * +# * Contributors: +# * Lars Vogel (Vogella GmbH) - initial implementation +# *******************************************************************************/ + +# Preference main page +JSonPreferencePage_maxItemsComputed=The maximum number of outline symbols and folding\nregions computed (limited for performance reasons, 0 means no limit). diff --git a/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/json/ui/preferences/JSonPreferenceInitializer.java b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/json/ui/preferences/JSonPreferenceInitializer.java new file mode 100644 index 0000000000..ddbeb66e95 --- /dev/null +++ b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/json/ui/preferences/JSonPreferenceInitializer.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) 2026 Vogella GmbH and others. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Lars Vogel (Vogella GmbH) - initial implementation + *******************************************************************************/ +package org.eclipse.wildwebdeveloper.json.ui.preferences; + +import org.eclipse.core.runtime.preferences.AbstractPreferenceInitializer; + +/** + * JSON preference initializer. + * + */ +public class JSonPreferenceInitializer extends AbstractPreferenceInitializer { + + @Override + public void initializeDefaultPreferences() { + JSonPreferenceServerConstants.initializeDefaultPreferences(); + } + +} diff --git a/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/json/ui/preferences/JSonPreferencePage.java b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/json/ui/preferences/JSonPreferencePage.java new file mode 100644 index 0000000000..01fb424d08 --- /dev/null +++ b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/json/ui/preferences/JSonPreferencePage.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2026 Vogella GmbH and others. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Lars Vogel (Vogella GmbH) - initial implementation + *******************************************************************************/ +package org.eclipse.wildwebdeveloper.json.ui.preferences; + +import static org.eclipse.wildwebdeveloper.json.ui.preferences.JSonPreferenceServerConstants.JSON_PREFERENCES_MAXITEMSCOMPUTED; + +import org.eclipse.jface.preference.FieldEditorPreferencePage; +import org.eclipse.jface.preference.IntegerFieldEditor; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPreferencePage; +import org.eclipse.wildwebdeveloper.Activator; +import org.eclipse.wildwebdeveloper.json.ui.Messages; + +/** + * JSON main preference page. + * + */ +public class JSonPreferencePage extends FieldEditorPreferencePage implements IWorkbenchPreferencePage { + + public JSonPreferencePage() { + super(GRID); + } + + @Override + public void init(IWorkbench workbench) { + setPreferenceStore(Activator.getDefault().getPreferenceStore()); + } + + @Override + protected void createFieldEditors() { + IntegerFieldEditor maxItemsComputed = new IntegerFieldEditor(JSON_PREFERENCES_MAXITEMSCOMPUTED, + Messages.JSonPreferencePage_maxItemsComputed, getFieldEditorParent()); + maxItemsComputed.setValidRange(0, Integer.MAX_VALUE); + addField(maxItemsComputed); + } + +} diff --git a/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/json/ui/preferences/JSonPreferenceServerConstants.java b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/json/ui/preferences/JSonPreferenceServerConstants.java new file mode 100644 index 0000000000..b4d3c3782b --- /dev/null +++ b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/json/ui/preferences/JSonPreferenceServerConstants.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * Copyright (c) 2026 Vogella GmbH and others. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Lars Vogel (Vogella GmbH) - initial implementation + *******************************************************************************/ +package org.eclipse.wildwebdeveloper.json.ui.preferences; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.wildwebdeveloper.Activator; +import org.eclipse.wildwebdeveloper.ui.preferences.Settings; + +/** + * JSON preference server constants. + * + */ +public class JSonPreferenceServerConstants { + + private static final String JSON_SECTION = "json"; //$NON-NLS-1$ + + /** + * Equivalent of VSCode's json.maxItemsComputed. Also drives the folding limits + * below, as VSCode does. 0 means no limit. + */ + public static final String JSON_PREFERENCES_MAXITEMSCOMPUTED = "json.resultLimit"; //$NON-NLS-1$ + + private static final String JSON_PREFERENCES_JSON_FOLDINGLIMIT = "json.jsonFoldingLimit"; //$NON-NLS-1$ + private static final String JSON_PREFERENCES_JSONC_FOLDINGLIMIT = "json.jsoncFoldingLimit"; //$NON-NLS-1$ + private static final String JSON_PREFERENCES_VALIDATE_ENABLE = "json.validate.enable"; //$NON-NLS-1$ + private static final String JSON_PREFERENCES_FORMAT_ENABLE = "json.format.enable"; //$NON-NLS-1$ + private static final String HTTP_PREFERENCES_PROXYSTRICTSSL = "http.proxyStrictSSL"; //$NON-NLS-1$ + + public static final int MAXITEMSCOMPUTED_DEFAULT = 5000; + + /** + * The server re-reads all of its settings from every notification, so entries + * that only restate a default still have to be sent: without "validate.enable" + * it turns validation off, and without "http.proxyStrictSSL" it stops verifying + * certificates when downloading schemas. + */ + public static Settings getGlobalSettings() { + IPreferenceStore store = Activator.getDefault().getPreferenceStore(); + Settings settings = new Settings(store); + + int maxItemsComputed = store.getInt(JSON_PREFERENCES_MAXITEMSCOMPUTED); + settings.fillSetting(JSON_PREFERENCES_MAXITEMSCOMPUTED, maxItemsComputed); + settings.fillSetting(JSON_PREFERENCES_JSON_FOLDINGLIMIT, maxItemsComputed); + settings.fillSetting(JSON_PREFERENCES_JSONC_FOLDINGLIMIT, maxItemsComputed); + + settings.fillSetting(JSON_PREFERENCES_VALIDATE_ENABLE, Boolean.TRUE); + settings.fillSetting(JSON_PREFERENCES_FORMAT_ENABLE, Boolean.TRUE); + settings.fillSetting(HTTP_PREFERENCES_PROXYSTRICTSSL, Boolean.TRUE); + + return settings; + } + + public static void initializeDefaultPreferences() { + IPreferenceStore store = Activator.getDefault().getPreferenceStore(); + store.setDefault(JSON_PREFERENCES_MAXITEMSCOMPUTED, MAXITEMSCOMPUTED_DEFAULT); + } + + public static boolean isMatchJSonSection(String section) { + return Settings.isMatchSection(section, JSON_SECTION); + } + +}