Skip to content

Commit 5c30041

Browse files
committed
[#242] Make ClangdConfigurationFileManager public API
since its needed by some vendors to overwrite certain methods it should be made available. part of #276
1 parent 1d67a19 commit 5c30041

File tree

5 files changed

+238
-6
lines changed

5 files changed

+238
-6
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ Require-Bundle: org.eclipse.cdt.lsp;bundle-version="0.0.0",
2828
org.eclipse.ui.workbench.texteditor;bundle-version="0.0.0",
2929
org.eclipse.core.variables;bundle-version="0.0.0",
3030
org.yaml.snakeyaml;bundle-version="0.0.0"
31-
Service-Component: OSGI-INF/org.eclipse.cdt.lsp.clangd.internal.config.BuiltinClangdOptionsDefaults.xml,
31+
Service-Component: OSGI-INF/org.eclipse.cdt.lsp.clangd.ClangdConfigurationFileManager.xml,
32+
OSGI-INF/org.eclipse.cdt.lsp.clangd.internal.config.BuiltinClangdOptionsDefaults.xml,
3233
OSGI-INF/org.eclipse.cdt.lsp.clangd.internal.config.ClangdConfigurationAccess.xml,
33-
OSGI-INF/org.eclipse.cdt.lsp.clangd.internal.config.ClangdConfigurationFileManager.xml,
3434
OSGI-INF/org.eclipse.cdt.lsp.clangd.internal.config.ClangdFallbackManager.xml,
3535
OSGI-INF/org.eclipse.cdt.lsp.clangd.internal.config.ClangdMetadataDefaults.xml
3636
Bundle-Activator: org.eclipse.cdt.lsp.clangd.plugin.ClangdPlugin
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<?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.internal.config.ClangdConfigurationFileManager">
2+
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.3.0" name="org.eclipse.cdt.lsp.clangd.ClangdConfigurationFileManager">
33
<property name="service.ranking" type="Integer" value="0"/>
44
<service>
55
<provide interface="org.eclipse.cdt.lsp.clangd.ClangdCProjectDescriptionListener"/>
66
</service>
77
<reference cardinality="1..1" field="build" interface="org.eclipse.cdt.core.build.ICBuildConfigurationManager" name="build"/>
8-
<implementation class="org.eclipse.cdt.lsp.clangd.internal.config.ClangdConfigurationFileManager"/>
8+
<implementation class="org.eclipse.cdt.lsp.clangd.ClangdConfigurationFileManager"/>
99
</scr:component>
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
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.build.CBuildConfiguration;
23+
import org.eclipse.cdt.core.build.ICBuildConfiguration;
24+
import org.eclipse.cdt.core.build.ICBuildConfigurationManager;
25+
import org.eclipse.cdt.core.cdtvariables.CdtVariableException;
26+
import org.eclipse.cdt.core.settings.model.CProjectDescriptionEvent;
27+
import org.eclipse.cdt.core.settings.model.ICConfigurationDescription;
28+
import org.eclipse.cdt.core.settings.model.ICProjectDescription;
29+
import org.eclipse.cdt.lsp.plugin.LspPlugin;
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.NullProgressMonitor;
35+
import org.eclipse.core.runtime.Platform;
36+
import org.osgi.service.component.annotations.Component;
37+
import org.osgi.service.component.annotations.Reference;
38+
import org.yaml.snakeyaml.Yaml;
39+
import org.yaml.snakeyaml.scanner.ScannerException;
40+
41+
/**
42+
* Default implementation of the {@link ClangdCProjectDescriptionListener}.
43+
* Can be extended by vendors if needed. This implementation sets the path to
44+
* the compile_commands.json in the .clangd file in the projects root directory.
45+
* This is needed by CDT projects since the compile_commands.json is generated in the build folder.
46+
* When the active build configuration changes in managed build projects, this manager updates the path to the database in
47+
* the .clangd file to ensure that clangd uses the compile_commads.json of the active build configuration.
48+
*
49+
* This class can be extended by vendors.
50+
*/
51+
@Component(property = { "service.ranking:Integer=0" })
52+
public class ClangdConfigurationFileManager implements ClangdCProjectDescriptionListener {
53+
public static final String CLANGD_CONFIG_FILE_NAME = ".clangd"; //$NON-NLS-1$
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+
@Reference
60+
private ICBuildConfigurationManager build;
61+
62+
@Override
63+
public void handleEvent(CProjectDescriptionEvent event, MacroResolver macroResolver) {
64+
setCompilationDatabasePath(event.getProject(), event.getNewCProjectDescription(), macroResolver);
65+
}
66+
67+
/**
68+
* Set the <code>CompilationDatabase</code> entry in the <code>.clangd</code> file which is located in the <code>project</code> root,
69+
* if the yaml file syntax can be parsed.
70+
* The <code>.clangd</code> file will be created, if it's not existing.
71+
* The <code>CompilationDatabase</code> points to the build folder of the active build configuration
72+
* (in case <code>project</code> is a managed C/C++ project).
73+
*
74+
* In the following example clangd uses the compile_commands.json file in the Debug folder:
75+
* <pre>CompileFlags: {CompilationDatabase: Debug}</pre>
76+
*
77+
* @param project C/C++ project
78+
* @param newCProjectDescription new CProject description
79+
* @param macroResolver helper to resolve macros in the CWD path of the builder
80+
*/
81+
protected void setCompilationDatabasePath(IProject project, ICProjectDescription newCProjectDescription,
82+
MacroResolver macroResolver) {
83+
if (project != null && newCProjectDescription != null) {
84+
if (enableSetCompilationDatabasePath(project)) {
85+
var relativeDatabasePath = getRelativeDatabasePath(project, newCProjectDescription, macroResolver);
86+
if (!relativeDatabasePath.isEmpty()) {
87+
setCompilationDatabase(project, relativeDatabasePath);
88+
} else {
89+
Platform.getLog(getClass()).error("Cannot determine path to compile_commands.json"); //$NON-NLS-1$
90+
}
91+
}
92+
}
93+
}
94+
95+
/**
96+
* Enabler for {@link setCompilationDatabasePath}. Can be overriden for customization.
97+
* @param project
98+
* @return true if the database path should be written to .clangd file in the project root.
99+
*/
100+
protected boolean enableSetCompilationDatabasePath(IProject project) {
101+
return Optional.ofNullable(LspPlugin.getDefault()).map(LspPlugin::getCLanguageServerProvider)
102+
.map(provider -> provider.isEnabledFor(project)).orElse(Boolean.FALSE);
103+
}
104+
105+
/**
106+
* Get project relative path to compile_commands.json file.
107+
* By de
108+
* @param project
109+
* @param newCProjectDescription
110+
* @param macroResolver
111+
* @return project relative path to active build folder or empty String
112+
*/
113+
private String getRelativeDatabasePath(IProject project, ICProjectDescription newCProjectDescription,
114+
MacroResolver macroResolver) {
115+
if (project != null && newCProjectDescription != null) {
116+
ICConfigurationDescription config = newCProjectDescription.getDefaultSettingConfiguration();
117+
var cwdBuilder = config.getBuildSetting().getBuilderCWD();
118+
var projectLocation = project.getLocation().addTrailingSeparator().toOSString();
119+
if (cwdBuilder != null) {
120+
try {
121+
var cwdString = macroResolver.resolveValue(cwdBuilder.toOSString(), EMPTY, null, config);
122+
return cwdString.replace(projectLocation, EMPTY);
123+
} catch (CdtVariableException e) {
124+
Platform.getLog(getClass()).log(e.getStatus());
125+
}
126+
} else {
127+
//it is probably a cmake project:
128+
return buildConfiguration(project)//
129+
.filter(CBuildConfiguration.class::isInstance)//
130+
.map(bc -> {
131+
try {
132+
return ((CBuildConfiguration) bc).getBuildContainer();
133+
} catch (CoreException e) {
134+
Platform.getLog(getClass()).log(e.getStatus());
135+
}
136+
return null;
137+
})//
138+
.map(c -> c.getLocation())//
139+
.map(l -> l.toOSString().replace(projectLocation, EMPTY)).orElse(EMPTY);
140+
}
141+
}
142+
return EMPTY;
143+
}
144+
145+
private Optional<ICBuildConfiguration> buildConfiguration(IResource initial) {
146+
try {
147+
var active = initial.getProject().getActiveBuildConfig();
148+
if (active != null && build != null) {
149+
return Optional.ofNullable(build.getBuildConfiguration(active));
150+
}
151+
} catch (CoreException e) {
152+
Platform.getLog(getClass()).error(e.getMessage(), e);
153+
}
154+
return Optional.empty();
155+
}
156+
157+
/**
158+
* Set the <code>CompilationDatabase</code> entry in the .clangd file in the given project root.
159+
* The file will be created, if it's not existing.
160+
* A ScannerException will be thrown if the configuration file contains invalid yaml syntax.
161+
*
162+
* @param project to write the .clangd file
163+
* @param databasePath project relative path to .clangd file
164+
* @throws IOException
165+
* @throws ScannerException
166+
* @throws CoreException
167+
*/
168+
@SuppressWarnings("unchecked")
169+
public void setCompilationDatabase(IProject project, String databasePath) {
170+
var configFile = project.getFile(CLANGD_CONFIG_FILE_NAME);
171+
try {
172+
if (createClangdConfigFile(configFile, project.getDefaultCharset(), databasePath, false)) {
173+
return;
174+
}
175+
Map<String, Object> data = null;
176+
Yaml yaml = new Yaml();
177+
try (var inputStream = configFile.getContents()) {
178+
//throws ScannerException and ParserException:
179+
try {
180+
data = yaml.load(inputStream);
181+
} catch (Exception e) {
182+
Platform.getLog(getClass()).error(e.getMessage(), e);
183+
// return, since the file syntax is corrupted. The user has to fix it first:
184+
return;
185+
}
186+
}
187+
if (data == null) {
188+
//empty file: (re)create .clangd file:
189+
createClangdConfigFile(configFile, project.getDefaultCharset(), databasePath, true);
190+
return;
191+
}
192+
Map<String, Object> map = (Map<String, Object>) data.get(COMPILE_FLAGS);
193+
if (map != null) {
194+
var cdb = map.get(COMPILATTION_DATABASE);
195+
if (cdb != null && cdb instanceof String) {
196+
if (cdb.equals(databasePath)) {
197+
return;
198+
}
199+
}
200+
map.put(COMPILATTION_DATABASE, databasePath);
201+
data.put(COMPILE_FLAGS, map);
202+
try (var yamlWriter = new PrintWriter(configFile.getLocation().toFile())) {
203+
yaml.dump(data, yamlWriter);
204+
}
205+
}
206+
} catch (CoreException e) {
207+
Platform.getLog(getClass()).log(e.getStatus());
208+
} catch (IOException e) {
209+
Platform.getLog(getClass()).error(e.getMessage(), e);
210+
}
211+
}
212+
213+
private boolean createClangdConfigFile(IFile configFile, String charset, String databasePath,
214+
boolean overwriteContent) {
215+
if (!configFile.exists() || overwriteContent) {
216+
try (final var data = new ByteArrayInputStream(
217+
String.format(SET_COMPILATION_DB, databasePath).getBytes(charset))) {
218+
if (overwriteContent) {
219+
configFile.setContents(data, IResource.KEEP_HISTORY, new NullProgressMonitor());
220+
} else {
221+
configFile.create(data, false, new NullProgressMonitor());
222+
}
223+
return true;
224+
} catch (CoreException e) {
225+
Platform.getLog(getClass()).log(e.getStatus());
226+
} catch (IOException e) {
227+
Platform.getLog(getClass()).error(e.getMessage(), e);
228+
}
229+
}
230+
return false;
231+
}
232+
}

tests/org.eclipse.cdt.lsp.clangd.tests/src/org/eclipse/cdt/lsp/clangd/tests/ClangdConfigurationFileManagerTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
import org.eclipse.cdt.core.settings.model.ICProjectDescription;
3434
import org.eclipse.cdt.internal.core.settings.model.CConfigurationDescriptionCache;
3535
import org.eclipse.cdt.lsp.clangd.ClangdCProjectDescriptionListener;
36-
import org.eclipse.cdt.lsp.clangd.internal.config.ClangdConfigurationFileManager;
36+
import org.eclipse.cdt.lsp.clangd.ClangdConfigurationFileManager;
3737
import org.eclipse.core.resources.IFile;
3838
import org.eclipse.core.resources.IProject;
3939
import org.eclipse.core.resources.IResource;

tests/org.eclipse.cdt.lsp.clangd.tests/src/org/eclipse/cdt/lsp/internal/clangd/tests/ClangdConfigFileCheckerTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
import java.io.IOException;
2121
import java.io.UnsupportedEncodingException;
2222

23+
import org.eclipse.cdt.lsp.clangd.ClangdConfigurationFileManager;
2324
import org.eclipse.cdt.lsp.clangd.internal.config.ClangdConfigFileChecker;
2425
import org.eclipse.cdt.lsp.clangd.internal.config.ClangdConfigFileMonitor;
25-
import org.eclipse.cdt.lsp.clangd.internal.config.ClangdConfigurationFileManager;
2626
import org.eclipse.cdt.lsp.clangd.tests.TestUtils;
2727
import org.eclipse.core.resources.IFile;
2828
import org.eclipse.core.resources.IMarker;

0 commit comments

Comments
 (0)