Skip to content

Commit 67c4d24

Browse files
committed
[#227] Refactor ClangdConfigurationManager
- Allows vendors to overwrite default behavior fixes #227
1 parent 675c87f commit 67c4d24

16 files changed

+547
-216
lines changed

bundles/org.eclipse.cdt.lsp.clangd/META-INF/MANIFEST.MF

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ Require-Bundle: org.eclipse.cdt.lsp;bundle-version="0.0.0",
3232
org.eclipse.ui.workbench.texteditor;bundle-version="0.0.0",
3333
org.eclipse.core.variables
3434
Service-Component: OSGI-INF/org.eclipse.cdt.lsp.clangd.BuiltinClangdOptionsDefaults.xml,
35+
OSGI-INF/org.eclipse.cdt.lsp.clangd.ClangdConfigurationManager.xml,
36+
OSGI-INF/org.eclipse.cdt.lsp.clangd.DefaultCProjectChangeMonitor.xml,
3537
OSGI-INF/org.eclipse.cdt.lsp.internal.clangd.ClangdConfigurationAccess.xml,
3638
OSGI-INF/org.eclipse.cdt.lsp.internal.clangd.ClangdFallbackManager.xml,
37-
OSGI-INF/org.eclipse.cdt.lsp.internal.clangd.ClangdMetadataDefaults.xml
39+
OSGI-INF/org.eclipse.cdt.lsp.internal.clangd.ClangdMetadataDefaults.xml,
40+
OSGI-INF/org.eclipse.cdt.lsp.internal.clangd.DefaultMacroResolver.xml
3841
Bundle-Activator: org.eclipse.cdt.lsp.internal.clangd.editor.ClangdPlugin
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="org.eclipse.cdt.lsp.clangd.ClangdConfigurationManager">
3+
<property name="service.ranking" type="Integer" value="0"/>
4+
<service>
5+
<provide interface="org.eclipse.cdt.lsp.clangd.ClangdCProjectDescriptionListener"/>
6+
</service>
7+
<implementation class="org.eclipse.cdt.lsp.clangd.ClangdConfigurationManager"/>
8+
</scr:component>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.3.0" name="org.eclipse.cdt.lsp.clangd.DefaultCProjectChangeMonitor">
3+
<property name="service.ranking" type="Integer" value="0"/>
4+
<service>
5+
<provide interface="org.eclipse.cdt.lsp.clangd.CProjectChangeMonitor"/>
6+
</service>
7+
<reference cardinality="1..1" field="clangdListener" interface="org.eclipse.cdt.lsp.clangd.ClangdCProjectDescriptionListener" name="clangdListener"/>
8+
<reference cardinality="1..1" field="macroResolver" interface="org.eclipse.cdt.lsp.clangd.MacroResolver" name="macroResolver"/>
9+
<implementation class="org.eclipse.cdt.lsp.clangd.DefaultCProjectChangeMonitor"/>
10+
</scr:component>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="org.eclipse.cdt.lsp.internal.clangd.DefaultMacroResolver">
3+
<property name="service.ranking" type="Integer" value="0"/>
4+
<service>
5+
<provide interface="org.eclipse.cdt.lsp.clangd.MacroResolver"/>
6+
</service>
7+
<implementation class="org.eclipse.cdt.lsp.internal.clangd.DefaultMacroResolver"/>
8+
</scr:component>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2024 Bachmann electronic GmbH and others.
3+
*
4+
* This program and the accompanying materials are made
5+
* available under the terms of the Eclipse Public License 2.0
6+
* which is available at https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*
10+
* Contributors:
11+
* Gesa Hentschke (Bachmann electronic GmbH) - initial implementation
12+
*******************************************************************************/
13+
14+
package org.eclipse.cdt.lsp.clangd;
15+
16+
/**
17+
* This monitor gets started when this plugin is loaded and stopped on a plugin stop respectively.
18+
*/
19+
public interface CProjectChangeMonitor {
20+
21+
/**
22+
* Listeners can be added in this method. It gets called on plugin start.
23+
* @return CProjectChangeMonitor
24+
*/
25+
CProjectChangeMonitor start();
26+
27+
/**
28+
* Listeners can be removed in this method. It gets called on plugin stop.
29+
*/
30+
void stop();
31+
32+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2024 Bachmann electronic GmbH and others.
3+
*
4+
* This program and the accompanying materials are made
5+
* available under the terms of the Eclipse Public License 2.0
6+
* which is available at https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*
10+
* Contributors:
11+
* Gesa Hentschke (Bachmann electronic GmbH) - initial implementation
12+
*******************************************************************************/
13+
14+
package org.eclipse.cdt.lsp.clangd;
15+
16+
import org.eclipse.cdt.core.settings.model.CProjectDescriptionEvent;
17+
import org.eclipse.cdt.core.settings.model.ICProjectDescription;
18+
import org.eclipse.core.resources.IProject;
19+
20+
/**
21+
* Vendors can implement this interface as OSGi service
22+
* with a service.ranking property > 0 to implement custom behavior
23+
* and to replace the {@link ClangdConfigurationManager}
24+
*/
25+
public interface ClangdCProjectDescriptionListener {
26+
String CLANGD_CONFIG_FILE_NAME = ".clangd"; //$NON-NLS-1$
27+
28+
/**
29+
* Called when the configuration of a CDT C/C++ project changes.
30+
* @param event
31+
* @param macroResolver
32+
*/
33+
void handleEvent(CProjectDescriptionEvent event, MacroResolver macroResolver);
34+
35+
/**
36+
* Set the <code>CompilationDatabase</code> entry in the <code>.clangd</code> file which is located in the <code>project</code> root.
37+
* The <code>.clangd</code> file will be created, if it's not existing.
38+
* The <code>CompilationDatabase</code> points to the build folder of the active build configuration
39+
* (in case <code>project</code> is a managed C/C++ project).
40+
*
41+
* In the following example clangd uses the compile_commands.json file in the Debug folder:
42+
* <pre>CompileFlags: {CompilationDatabase: Debug}</pre>
43+
*
44+
* @param project managed C/C++ project
45+
* @param newCProjectDescription new CProject description
46+
* @param macroResolver helper to resolve macros in the CWD path of the builder
47+
*/
48+
void setCompilationDatabasePath(IProject project, ICProjectDescription newCProjectDescription,
49+
MacroResolver macroResolver);
50+
51+
/**
52+
* Enabler for {@link setCompilationDatabasePath}. Can be overriden for customization.
53+
* @param project
54+
* @return true if the database path should be written to .clangd file in the project root.
55+
*/
56+
boolean enableSetCompilationDatabasePath(IProject project);
57+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2024 Bachmann electronic GmbH and others.
3+
*
4+
* This program and the accompanying materials are made
5+
* available under the terms of the Eclipse Public License 2.0
6+
* which is available at https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*
10+
* Contributors:
11+
* Gesa Hentschke (Bachmann electronic GmbH) - initial implementation
12+
*******************************************************************************/
13+
14+
package org.eclipse.cdt.lsp.clangd;
15+
16+
import java.io.ByteArrayInputStream;
17+
import java.io.IOException;
18+
import java.io.PrintWriter;
19+
import java.util.Map;
20+
import java.util.Optional;
21+
22+
import org.eclipse.cdt.core.cdtvariables.CdtVariableException;
23+
import org.eclipse.cdt.core.settings.model.CProjectDescriptionEvent;
24+
import org.eclipse.cdt.core.settings.model.ICConfigurationDescription;
25+
import org.eclipse.cdt.core.settings.model.ICProjectDescription;
26+
import org.eclipse.cdt.lsp.LspPlugin;
27+
import org.eclipse.cdt.lsp.LspUtils;
28+
import org.eclipse.cdt.lsp.internal.clangd.editor.ClangdPlugin;
29+
import org.eclipse.cdt.lsp.internal.clangd.editor.LspEditorUiMessages;
30+
import org.eclipse.core.resources.IFile;
31+
import org.eclipse.core.resources.IProject;
32+
import org.eclipse.core.resources.IResource;
33+
import org.eclipse.core.runtime.CoreException;
34+
import org.eclipse.core.runtime.IStatus;
35+
import org.eclipse.core.runtime.NullProgressMonitor;
36+
import org.eclipse.core.runtime.Platform;
37+
import org.eclipse.core.runtime.Status;
38+
import org.osgi.service.component.annotations.Component;
39+
import org.yaml.snakeyaml.Yaml;
40+
import org.yaml.snakeyaml.scanner.ScannerException;
41+
42+
/**
43+
* Default implementation of the {@link ClangdCProjectDescriptionListener}.
44+
* Can be replaced by vendors if needed. This implementation sets the path to
45+
* the compile_commands.json in the .clangd file in the projects root directory.
46+
* This is needed by CDT projects since the compile_commands.json is generated in the build folder.
47+
* When the active build configuration changes in managed build projects, this manager updates the path to the database in
48+
* the .clangd file to ensure that clangd uses the compile_commads.json of the active build configuration.
49+
*
50+
* This class can be extended by vendors.
51+
*/
52+
@Component(property = { "service.ranking:Integer=0" })
53+
public class ClangdConfigurationManager implements ClangdCProjectDescriptionListener {
54+
private static final String COMPILE_FLAGS = "CompileFlags"; //$NON-NLS-1$
55+
private static final String COMPILATTION_DATABASE = "CompilationDatabase"; //$NON-NLS-1$
56+
private static final String SET_COMPILATION_DB = COMPILE_FLAGS + ": {" + COMPILATTION_DATABASE + ": %s}"; //$NON-NLS-1$ //$NON-NLS-2$
57+
private static final String EMPTY = ""; //$NON-NLS-1$
58+
59+
@Override
60+
public void handleEvent(CProjectDescriptionEvent event, MacroResolver macroResolver) {
61+
setCompilationDatabasePath(event.getProject(), event.getNewCProjectDescription(), macroResolver);
62+
}
63+
64+
@Override
65+
public void setCompilationDatabasePath(IProject project, ICProjectDescription newCProjectDescription,
66+
MacroResolver macroResolver) {
67+
if (project != null && newCProjectDescription != null) {
68+
if (enableSetCompilationDatabasePath(project)) {
69+
var relativeDatabasePath = getRelativeDatabasePath(project, newCProjectDescription, macroResolver);
70+
if (!relativeDatabasePath.isEmpty()) {
71+
try {
72+
setCompilationDatabase(project, relativeDatabasePath);
73+
} catch (ScannerException e) {
74+
var status = new Status(IStatus.ERROR, ClangdPlugin.PLUGIN_ID, e.getMessage());
75+
var projectLocation = project.getLocation().addTrailingSeparator().toPortableString();
76+
LspUtils.showErrorMessage(LspEditorUiMessages.CProjectChangeMonitor_yaml_scanner_error,
77+
LspEditorUiMessages.CProjectChangeMonitor_yaml_scanner_error_message + projectLocation
78+
+ CLANGD_CONFIG_FILE_NAME,
79+
status);
80+
}
81+
} else {
82+
Platform.getLog(getClass()).log(new Status(Status.ERROR, ClangdPlugin.PLUGIN_ID,
83+
"Cannot determine path to compile_commands.json")); //$NON-NLS-1$
84+
}
85+
}
86+
}
87+
}
88+
89+
@Override
90+
public boolean enableSetCompilationDatabasePath(IProject project) {
91+
return Optional.ofNullable(LspPlugin.getDefault()).map(LspPlugin::getCLanguageServerProvider)
92+
.map(provider -> provider.isEnabledFor(project)).orElse(Boolean.FALSE);
93+
}
94+
95+
/**
96+
* Get project relative path to compile_commands.json file.
97+
* By de
98+
* @param project
99+
* @param newCProjectDescription
100+
* @param macroResolver
101+
* @return project relative path to active build folder or empty String
102+
*/
103+
private String getRelativeDatabasePath(IProject project, ICProjectDescription newCProjectDescription,
104+
MacroResolver macroResolver) {
105+
if (project != null && newCProjectDescription != null) {
106+
ICConfigurationDescription config = newCProjectDescription.getDefaultSettingConfiguration();
107+
var cwdBuilder = config.getBuildSetting().getBuilderCWD();
108+
if (cwdBuilder != null) {
109+
var projectLocation = project.getLocation().addTrailingSeparator().toOSString();
110+
try {
111+
var cwdString = macroResolver.resolveValue(cwdBuilder.toOSString(), EMPTY, null, config);
112+
return cwdString.replace(projectLocation, EMPTY);
113+
} catch (CdtVariableException e) {
114+
Platform.getLog(getClass()).log(e.getStatus());
115+
}
116+
}
117+
}
118+
return EMPTY;
119+
}
120+
121+
/**
122+
* Set the <code>CompilationDatabase</code> entry in the .clangd file in the given project root.
123+
* The file will be created, if it's not existing.
124+
* A ScannerException will be thrown if the configuration file contains invalid yaml syntax.
125+
*
126+
* @param project to write the .clangd file
127+
* @param databasePath project relative path to .clangd file
128+
* @throws IOException
129+
* @throws ScannerException
130+
* @throws CoreException
131+
*/
132+
@SuppressWarnings("unchecked")
133+
private void setCompilationDatabase(IProject project, String databasePath) {
134+
var configFile = project.getFile(CLANGD_CONFIG_FILE_NAME);
135+
try {
136+
if (createClangdConfigFile(configFile, project.getDefaultCharset(), databasePath, false)) {
137+
return;
138+
}
139+
Map<String, Object> data = null;
140+
Yaml yaml = new Yaml();
141+
try (var inputStream = configFile.getContents()) {
142+
//throws ScannerException
143+
data = yaml.load(inputStream);
144+
}
145+
if (data == null) {
146+
//empty file: (re)create .clangd file:
147+
createClangdConfigFile(configFile, project.getDefaultCharset(), databasePath, true);
148+
return;
149+
}
150+
Map<String, Object> map = (Map<String, Object>) data.get(COMPILE_FLAGS);
151+
if (map != null) {
152+
var cdb = map.get(COMPILATTION_DATABASE);
153+
if (cdb != null && cdb instanceof String) {
154+
if (cdb.equals(databasePath)) {
155+
return;
156+
}
157+
}
158+
map.put(COMPILATTION_DATABASE, databasePath);
159+
data.put(COMPILE_FLAGS, map);
160+
try (var yamlWriter = new PrintWriter(configFile.getLocation().toFile())) {
161+
yaml.dump(data, yamlWriter);
162+
}
163+
}
164+
} catch (CoreException e) {
165+
Platform.getLog(getClass()).log(e.getStatus());
166+
} catch (IOException e) {
167+
Platform.getLog(getClass()).error(e.getMessage(), e);
168+
}
169+
}
170+
171+
private boolean createClangdConfigFile(IFile configFile, String charset, String databasePath,
172+
boolean overwriteContent) {
173+
if (!configFile.exists() || overwriteContent) {
174+
try (final var data = new ByteArrayInputStream(
175+
String.format(SET_COMPILATION_DB, databasePath).getBytes(charset))) {
176+
if (overwriteContent) {
177+
configFile.setContents(data, IResource.KEEP_HISTORY, new NullProgressMonitor());
178+
} else {
179+
configFile.create(data, false, new NullProgressMonitor());
180+
}
181+
return true;
182+
} catch (CoreException e) {
183+
Platform.getLog(getClass()).log(e.getStatus());
184+
} catch (IOException e) {
185+
Platform.getLog(getClass()).error(e.getMessage(), e);
186+
}
187+
}
188+
return false;
189+
}
190+
191+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2024 Bachmann electronic GmbH and others.
3+
*
4+
* This program and the accompanying materials are made
5+
* available under the terms of the Eclipse Public License 2.0
6+
* which is available at https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*
10+
* Contributors:
11+
* Gesa Hentschke (Bachmann electronic GmbH) - initial implementation
12+
*******************************************************************************/
13+
14+
package org.eclipse.cdt.lsp.clangd;
15+
16+
import org.eclipse.cdt.core.CCorePlugin;
17+
import org.eclipse.cdt.core.settings.model.CProjectDescriptionEvent;
18+
import org.eclipse.cdt.core.settings.model.ICProjectDescriptionListener;
19+
import org.osgi.service.component.annotations.Component;
20+
import org.osgi.service.component.annotations.Reference;
21+
22+
/**
23+
* This default monitor listens to C project description changes. Can be derived by vendors to add own listeners/behavior.
24+
* This can be done by using this class as superclass and add the new class as OSGi service with a service.ranking > 0.
25+
*/
26+
@Component(property = { "service.ranking:Integer=0" })
27+
public class DefaultCProjectChangeMonitor implements CProjectChangeMonitor {
28+
29+
@Reference
30+
MacroResolver macroResolver;
31+
32+
@Reference
33+
private ClangdCProjectDescriptionListener clangdListener;
34+
35+
private final ICProjectDescriptionListener listener = new ICProjectDescriptionListener() {
36+
37+
@Override
38+
public void handleEvent(CProjectDescriptionEvent event) {
39+
clangdListener.handleEvent(event, macroResolver);
40+
}
41+
42+
};
43+
44+
@Override
45+
public CProjectChangeMonitor start() {
46+
CCorePlugin.getDefault().getProjectDescriptionManager().addCProjectDescriptionListener(listener,
47+
CProjectDescriptionEvent.APPLIED);
48+
return this;
49+
}
50+
51+
@Override
52+
public void stop() {
53+
CCorePlugin.getDefault().getProjectDescriptionManager().removeCProjectDescriptionListener(listener);
54+
}
55+
56+
}

0 commit comments

Comments
 (0)