diff --git a/bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/editor/EditorMetadata.java b/bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/editor/EditorMetadata.java index 5b429cef..83b6229b 100644 --- a/bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/editor/EditorMetadata.java +++ b/bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/editor/EditorMetadata.java @@ -17,6 +17,10 @@ import org.eclipse.core.runtime.preferences.PreferenceMetadata; public interface EditorMetadata extends ConfigurationMetadata { + /** + * @since 3.0 + */ + public static final String PREFER_LSP_KEY = "prefer_lsp"; //$NON-NLS-1$ /** * The predefined metadata for the "Prefer C/C++ Editor (LSP)" option @@ -26,8 +30,7 @@ public interface EditorMetadata extends ConfigurationMetadata { * @since 3.0 */ PreferenceMetadata preferLspEditor = new PreferenceMetadata<>(Boolean.class, // - "prefer_lsp", //$NON-NLS-1$ - false, // + PREFER_LSP_KEY, false, // LspUiMessages.LspEditorConfigurationPage_preferLspEditor, LspUiMessages.LspEditorConfigurationPage_preferLspEditor_description); diff --git a/bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/editor/EditorOptions.java b/bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/editor/EditorOptions.java index 8b95cd70..a6ae6ae0 100644 --- a/bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/editor/EditorOptions.java +++ b/bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/editor/EditorOptions.java @@ -12,6 +12,8 @@ package org.eclipse.cdt.lsp.editor; +import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener; + public interface EditorOptions { /** @@ -42,4 +44,14 @@ public interface EditorOptions { */ boolean formatEditedLines(); + /** + * @since 3.0 + */ + void addPreferenceChangedListener(IPreferenceChangeListener listener); + + /** + * @since 3.0 + */ + void removePreferenceChangedListener(IPreferenceChangeListener listener); + } diff --git a/bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/internal/editor/EditorPreferredOptions.java b/bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/internal/editor/EditorPreferredOptions.java index 32359f7f..d383aa77 100644 --- a/bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/internal/editor/EditorPreferredOptions.java +++ b/bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/internal/editor/EditorPreferredOptions.java @@ -12,11 +12,15 @@ package org.eclipse.cdt.lsp.internal.editor; +import java.util.Optional; + import org.eclipse.cdt.lsp.PreferredOptions; import org.eclipse.cdt.lsp.editor.EditorMetadata; import org.eclipse.cdt.lsp.editor.EditorOptions; import org.eclipse.cdt.lsp.editor.LanguageServerEnable; +import org.eclipse.cdt.lsp.internal.server.URIEnableCache; import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener; import org.eclipse.core.runtime.preferences.IScopeContext; public final class EditorPreferredOptions extends PreferredOptions implements EditorOptions, LanguageServerEnable { @@ -26,6 +30,7 @@ public EditorPreferredOptions(EditorMetadata metadata, String qualifier, IScopeC LanguageServerEnable enable) { super(metadata, qualifier, scopes); this.enable = enable; + this.addPreferenceChangedListener(URIEnableCache.getInstance()); } @Override @@ -56,4 +61,18 @@ public boolean isEnabledFor(IProject project) { return booleanValue(EditorMetadata.preferLspEditor); } + @Override + public void addPreferenceChangedListener(IPreferenceChangeListener listener) { + for (var scope : scopes) { + Optional.ofNullable(scope.getNode(qualifier)).ifPresent(n -> n.addPreferenceChangeListener(listener)); + } + } + + @Override + public void removePreferenceChangedListener(IPreferenceChangeListener listener) { + for (var scope : scopes) { + Optional.ofNullable(scope.getNode(qualifier)).ifPresent(n -> n.removePreferenceChangeListener(listener)); + } + } + } diff --git a/bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/internal/server/HasLanguageServerPropertyTester.java b/bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/internal/server/HasLanguageServerPropertyTester.java index a449adf8..a81ab6df 100644 --- a/bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/internal/server/HasLanguageServerPropertyTester.java +++ b/bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/internal/server/HasLanguageServerPropertyTester.java @@ -16,8 +16,6 @@ import java.io.File; import java.net.URI; -import java.util.HashMap; -import java.util.Map; import java.util.Optional; import org.eclipse.cdt.core.model.ICProject; @@ -39,8 +37,8 @@ public class HasLanguageServerPropertyTester extends PropertyTester { private final ICLanguageServerProvider cLanguageServerProvider; private final ServiceCaller initial; private final ServiceCaller workspace; + private final URIEnableCache cache = URIEnableCache.getInstance(); private Optional project; - private final Map cache = new HashMap<>(); public HasLanguageServerPropertyTester() { this.cLanguageServerProvider = LspPlugin.getDefault().getCLanguageServerProvider(); @@ -64,10 +62,10 @@ public boolean test(Object receiver, String property, Object[] args, Object expe } // when getProject is empty, it's an external file: Check if the file is already opened, if not check the active editor: var isEnabled = enabledFor(uri); + cache.put(uri, isEnabled); if (isEnabled) { initial.call(iu -> iu.register(uri)); } - cache.put(uri, isEnabled); return isEnabled; } else if (receiver instanceof ITranslationUnit) { // called to enable the LS based CSymbolsContentProvider: diff --git a/bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/internal/server/URIEnableCache.java b/bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/internal/server/URIEnableCache.java new file mode 100644 index 00000000..70aaac38 --- /dev/null +++ b/bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/internal/server/URIEnableCache.java @@ -0,0 +1,147 @@ +/******************************************************************************* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * 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: + * See git history + *******************************************************************************/ + +package org.eclipse.cdt.lsp.internal.server; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; + +import org.eclipse.cdt.internal.core.LRUCache; +import org.eclipse.cdt.lsp.editor.EditorMetadata; +import org.eclipse.core.internal.content.ContentTypeManager; +import org.eclipse.core.runtime.content.IContentTypeManager.ContentTypeChangeEvent; +import org.eclipse.core.runtime.content.IContentTypeManager.IContentTypeChangeListener; +import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener; +import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent; +import org.eclipse.lsp4e.LSPEclipseUtils; +import org.eclipse.ui.IPartListener; +import org.eclipse.ui.IWindowListener; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.internal.genericeditor.ExtensionBasedTextEditor; + +/** + * Caches the enable status for a given resource URI. Used by {@link HasLanguageServerPropertyTester#test(Object, String, Object[], Object)} + * The cache is getting cleared: on changes in the C/C++ content types or the prefer LSP editor option has been changed (workspace or project level). + * A resource URI shall be removed from the cache if it's getting closed in the editor. + */ +public final class URIEnableCache + implements IPreferenceChangeListener, IContentTypeChangeListener, IPartListener, IWindowListener { + private static final String C_SOURCE = "org.eclipse.cdt.core.cSource"; //$NON-NLS-1$ + private static final String CXX_SOURCE = "org.eclipse.cdt.core.cxxSource"; //$NON-NLS-1$ + private static final String C_HEADER = "org.eclipse.cdt.core.cHeader"; //$NON-NLS-1$ + private static final String CXX_HEADER = "org.eclipse.cdt.core.cxxHeader"; //$NON-NLS-1$ + private static final Map cache = Collections.synchronizedMap(new LRUCache<>(100)); + private static URIEnableCache instance = null; + + private URIEnableCache() { + ContentTypeManager.getInstance().addContentTypeChangeListener(this); + if (PlatformUI.isWorkbenchRunning()) { + var workbench = PlatformUI.getWorkbench(); + workbench.addWindowListener(this); + Arrays.stream(workbench.getWorkbenchWindows()).map(IWorkbenchWindow::getPages).flatMap(Arrays::stream) + .forEach(p -> p.addPartListener(this)); + } + } + + public static void stop() { + if (instance != null) { + var workbench = PlatformUI.getWorkbench(); + workbench.removeWindowListener(instance); + Arrays.stream(workbench.getWorkbenchWindows()).map(IWorkbenchWindow::getPages).flatMap(Arrays::stream) + .forEach(p -> p.removePartListener(instance)); + cache.clear(); + } + } + + public static synchronized URIEnableCache getInstance() { + if (instance == null) { + instance = new URIEnableCache(); + } + return instance; + } + + public Boolean get(URI uri) { + return cache.get(uri); + } + + public void put(URI uri, Boolean value) { + cache.put(uri, value); + } + + @Override + public void preferenceChange(PreferenceChangeEvent event) { + if (EditorMetadata.PREFER_LSP_KEY.contentEquals(event.getKey())) { + cache.clear(); + } + } + + @Override + public void contentTypeChanged(ContentTypeChangeEvent event) { + var id = event.getContentType().getId(); + if (C_SOURCE.contentEquals(id) || CXX_SOURCE.contentEquals(id) || C_HEADER.contentEquals(id) + || CXX_HEADER.contentEquals(id)) { + cache.clear(); + } + } + + @Override + public void partActivated(IWorkbenchPart part) { + // do nothing + } + + @Override + public void partBroughtToTop(IWorkbenchPart part) { + // do nothing + } + + @Override + public void partClosed(IWorkbenchPart part) { + if (part instanceof ExtensionBasedTextEditor editor) { + Optional.ofNullable(LSPEclipseUtils.toUri(editor.getEditorInput())).ifPresent(uri -> cache.remove(uri)); + } + } + + @Override + public void partDeactivated(IWorkbenchPart part) { + // do nothing + } + + @Override + public void partOpened(IWorkbenchPart part) { + // do nothing + } + + @Override + public void windowActivated(IWorkbenchWindow window) { + // do nothing + } + + @Override + public void windowDeactivated(IWorkbenchWindow window) { + // do nothing + } + + @Override + public void windowClosed(IWorkbenchWindow window) { + Arrays.stream(window.getPages()).forEach(p -> p.removePartListener(this)); + } + + @Override + public void windowOpened(IWorkbenchWindow window) { + Arrays.stream(window.getPages()).forEach(p -> p.addPartListener(this)); + } +} diff --git a/bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/plugin/LspPlugin.java b/bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/plugin/LspPlugin.java index 5da895f7..e3e6a187 100644 --- a/bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/plugin/LspPlugin.java +++ b/bundles/org.eclipse.cdt.lsp/src/org/eclipse/cdt/lsp/plugin/LspPlugin.java @@ -17,6 +17,7 @@ import java.util.logging.Logger; import org.eclipse.cdt.lsp.internal.server.CLanguageServerRegistry; +import org.eclipse.cdt.lsp.internal.server.URIEnableCache; import org.eclipse.cdt.lsp.server.ICLanguageServerProvider; import org.eclipse.ui.plugin.AbstractUIPlugin; import org.osgi.framework.BundleContext; @@ -58,6 +59,7 @@ public void start(BundleContext context) throws Exception { @Override public void stop(BundleContext context) throws Exception { + URIEnableCache.stop(); plugin = null; super.stop(context); }