Skip to content

Commit d4bca9e

Browse files
authored
[#247] Add .clangd configuration file syntax checker (#249)
[#247] Add .clangd configuration file syntax checker - Inform the user via markers in the .clangd file when the syntax cannot be parsed, because this leads to problems in the ClangdConfigurationFileManager. - Works for UTF-8 characters in text file. fixes #247
1 parent 61be8ca commit d4bca9e

File tree

6 files changed

+379
-1
lines changed

6 files changed

+379
-1
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ Require-Bundle: org.eclipse.cdt.lsp;bundle-version="0.0.0",
3030
org.eclipse.ui.ide;bundle-version="0.0.0",
3131
org.eclipse.ui.workbench;bundle-version="0.0.0",
3232
org.eclipse.ui.workbench.texteditor;bundle-version="0.0.0",
33-
org.eclipse.core.variables;bundle-version="0.0.0"
33+
org.eclipse.core.variables;bundle-version="0.0.0",
34+
org.yaml.snakeyaml;bundle-version="0.0.0"
3435
Service-Component: OSGI-INF/org.eclipse.cdt.lsp.clangd.BuiltinClangdOptionsDefaults.xml,
3536
OSGI-INF/org.eclipse.cdt.lsp.clangd.ClangdConfigurationFileManager.xml,
3637
OSGI-INF/org.eclipse.cdt.lsp.internal.clangd.ClangdConfigurationAccess.xml,

bundles/org.eclipse.cdt.lsp.clangd/plugin.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,5 +110,16 @@
110110
</command>
111111
</menuContribution>
112112
</extension>
113+
<extension
114+
id="org.eclipse.cdt.lsp.clangd.config.marker"
115+
name=".clangd yaml Problem"
116+
point="org.eclipse.core.resources.markers">
117+
<super
118+
type="org.eclipse.core.resources.problemmarker">
119+
</super>
120+
<persistent
121+
value="true">
122+
</persistent>
123+
</extension>
113124

114125
</plugin>
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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.internal.clangd;
15+
16+
import java.io.IOException;
17+
18+
import org.eclipse.cdt.lsp.internal.clangd.editor.ClangdPlugin;
19+
import org.eclipse.core.resources.IFile;
20+
import org.eclipse.core.resources.IMarker;
21+
import org.eclipse.core.resources.IResource;
22+
import org.eclipse.core.runtime.CoreException;
23+
import org.eclipse.core.runtime.Platform;
24+
import org.yaml.snakeyaml.Yaml;
25+
import org.yaml.snakeyaml.error.MarkedYAMLException;
26+
27+
/**
28+
* Checks the <code>.clangd</code> file for syntax errors and notifies the user via error markers in the file and Problems view.
29+
*/
30+
public class ClangdConfigFileChecker {
31+
public static final String CLANGD_MARKER = ClangdPlugin.PLUGIN_ID + ".config.marker"; //$NON-NLS-1$
32+
33+
/**
34+
* Checks if the .clangd file contains valid yaml syntax. Adds error marker to the file if not.
35+
* @param configFile
36+
*/
37+
public void checkConfigFile(IFile configFile) {
38+
if (!configFile.exists()) {
39+
return;
40+
}
41+
Yaml yaml = new Yaml();
42+
try (var inputStream = configFile.getContents()) {
43+
try {
44+
removeMarkerFromClangdConfig(configFile);
45+
//throws ScannerException and ParserException:
46+
yaml.load(inputStream);
47+
} catch (MarkedYAMLException yamlException) {
48+
// re-read the file, because the buffer which comes along with MarkedYAMLException is limited to ~800 bytes.
49+
try (var reReadStream = configFile.getContents()) {
50+
addMarkerToClangdConfig(configFile, yamlException, reReadStream.readAllBytes());
51+
}
52+
} catch (Exception exception) {
53+
//log unexpected exception:
54+
Platform.getLog(getClass()).error("Expected MarkedYAMLException, but was: " + exception.getMessage(), //$NON-NLS-1$
55+
exception);
56+
}
57+
} catch (IOException | CoreException e) {
58+
Platform.getLog(getClass()).error(e.getMessage(), e);
59+
}
60+
}
61+
62+
private void addMarkerToClangdConfig(IFile configFile, MarkedYAMLException yamlException, byte[] buffer) {
63+
try {
64+
var configMarker = parseYamlException(yamlException, buffer);
65+
var marker = configFile.createMarker(CLANGD_MARKER);
66+
marker.setAttribute(IMarker.MESSAGE, configMarker.message);
67+
marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_ERROR);
68+
marker.setAttribute(IMarker.LINE_NUMBER, configMarker.line);
69+
marker.setAttribute(IMarker.CHAR_START, configMarker.charStart);
70+
marker.setAttribute(IMarker.CHAR_END, configMarker.charEnd);
71+
} catch (CoreException core) {
72+
Platform.getLog(getClass()).log(core.getStatus());
73+
}
74+
}
75+
76+
private void removeMarkerFromClangdConfig(IFile configFile) {
77+
try {
78+
configFile.deleteMarkers(CLANGD_MARKER, false, IResource.DEPTH_ZERO);
79+
} catch (CoreException e) {
80+
Platform.getLog(getClass()).log(e.getStatus());
81+
}
82+
}
83+
84+
private class ClangdConfigMarker {
85+
public String message;
86+
public int line = 1;
87+
public int charStart = -1;
88+
public int charEnd = -1;
89+
}
90+
91+
/**
92+
* Fetch line and char position information from exception to create a marker for the .clangd file.
93+
* @param exception
94+
* @param file
95+
* @return
96+
*/
97+
private ClangdConfigMarker parseYamlException(MarkedYAMLException exception, byte[] buffer) {
98+
var marker = new ClangdConfigMarker();
99+
marker.message = exception.getProblem();
100+
var problemMark = exception.getProblemMark();
101+
if (problemMark == null) {
102+
return marker;
103+
}
104+
marker.line = problemMark.getLine() + 1; //getLine() is zero based, IMarker wants 1-based
105+
int index = problemMark.getIndex();
106+
if (index == buffer.length) {
107+
// When index == buffer.length() the marker index points to the non visible
108+
// \r or \n character and the marker is not displayed in the editor.
109+
// Or, even worse, there is no next line and index + 1 would be > buffer.length
110+
// Therefore we have to find the last visible char:
111+
index = getIndexOfLastVisibleChar(buffer);
112+
}
113+
marker.charStart = index;
114+
marker.charEnd = index + 1;
115+
return marker;
116+
}
117+
118+
private int getIndexOfLastVisibleChar(byte[] buffer) {
119+
for (int i = buffer.length - 1; i >= 0; i--) {
120+
if ('\r' != ((char) buffer[i]) && '\n' != ((char) buffer[i])) {
121+
return i;
122+
}
123+
}
124+
return Math.max(0, buffer.length - 2);
125+
}
126+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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.internal.clangd;
15+
16+
import java.util.concurrent.ConcurrentLinkedQueue;
17+
18+
import org.eclipse.core.resources.IFile;
19+
import org.eclipse.core.resources.IResourceChangeEvent;
20+
import org.eclipse.core.resources.IResourceChangeListener;
21+
import org.eclipse.core.resources.IResourceDelta;
22+
import org.eclipse.core.resources.IWorkspace;
23+
import org.eclipse.core.resources.WorkspaceJob;
24+
import org.eclipse.core.runtime.CoreException;
25+
import org.eclipse.core.runtime.IProgressMonitor;
26+
import org.eclipse.core.runtime.IStatus;
27+
import org.eclipse.core.runtime.Platform;
28+
import org.eclipse.core.runtime.Status;
29+
30+
/**
31+
* Monitor changes in <code>.clangd</code> files in the workspace and triggers a yaml checker
32+
* to add error markers to the <code>.clangd</code> file when the edits causes yaml loader failures.
33+
*/
34+
public class ClangdConfigFileMonitor {
35+
private static final String CLANGD_CONFIG_FILE = ".clangd"; //$NON-NLS-1$
36+
private final ConcurrentLinkedQueue<IFile> pendingFiles = new ConcurrentLinkedQueue<>();
37+
private final IWorkspace workspace;
38+
private final ClangdConfigFileChecker checker = new ClangdConfigFileChecker();
39+
40+
private final IResourceChangeListener listener = new IResourceChangeListener() {
41+
@Override
42+
public void resourceChanged(IResourceChangeEvent event) {
43+
if (event.getDelta() != null && event.getType() == IResourceChangeEvent.POST_CHANGE) {
44+
try {
45+
event.getDelta().accept(delta -> {
46+
if ((delta.getKind() == IResourceDelta.ADDED
47+
|| (delta.getFlags() & IResourceDelta.CONTENT) != 0)
48+
&& CLANGD_CONFIG_FILE.equals(delta.getResource().getName())) {
49+
if (delta.getResource() instanceof IFile file) {
50+
pendingFiles.add(file);
51+
checkJob.schedule(100);
52+
}
53+
}
54+
return true;
55+
});
56+
} catch (CoreException e) {
57+
Platform.getLog(getClass()).log(e.getStatus());
58+
}
59+
}
60+
}
61+
};
62+
63+
public ClangdConfigFileMonitor(IWorkspace workspace) {
64+
this.workspace = workspace;
65+
}
66+
67+
private final WorkspaceJob checkJob = new WorkspaceJob("Check .clangd file") { //$NON-NLS-1$
68+
69+
@Override
70+
public IStatus runInWorkspace(IProgressMonitor monitor) throws CoreException {
71+
while (pendingFiles.peek() != null) {
72+
checker.checkConfigFile(pendingFiles.poll());
73+
}
74+
return Status.OK_STATUS;
75+
}
76+
77+
};
78+
79+
public ClangdConfigFileMonitor start() {
80+
workspace.addResourceChangeListener(listener);
81+
return this;
82+
}
83+
84+
public void stop() {
85+
workspace.removeResourceChangeListener(listener);
86+
}
87+
}

bundles/org.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/editor/ClangdPlugin.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package org.eclipse.cdt.lsp.internal.clangd.editor;
1616

1717
import org.eclipse.cdt.lsp.internal.clangd.CProjectChangeMonitor;
18+
import org.eclipse.cdt.lsp.internal.clangd.ClangdConfigFileMonitor;
1819
import org.eclipse.core.resources.IWorkspace;
1920
import org.eclipse.ui.plugin.AbstractUIPlugin;
2021
import org.osgi.framework.BundleContext;
@@ -27,6 +28,7 @@ public class ClangdPlugin extends AbstractUIPlugin {
2728
private IWorkspace workspace;
2829
private CompileCommandsMonitor compileCommandsMonitor;
2930
private CProjectChangeMonitor cProjectChangeMonitor;
31+
private ClangdConfigFileMonitor configFileMonitor;
3032

3133
// The plug-in ID
3234
public static final String PLUGIN_ID = "org.eclipse.cdt.lsp.clangd"; //$NON-NLS-1$
@@ -49,13 +51,15 @@ public void start(BundleContext context) throws Exception {
4951
workspace = workspaceTracker.getService();
5052
compileCommandsMonitor = new CompileCommandsMonitor(workspace).start();
5153
cProjectChangeMonitor = new CProjectChangeMonitor().start();
54+
configFileMonitor = new ClangdConfigFileMonitor(workspace).start();
5255
}
5356

5457
@Override
5558
public void stop(BundleContext context) throws Exception {
5659
plugin = null;
5760
compileCommandsMonitor.stop();
5861
cProjectChangeMonitor.stop();
62+
configFileMonitor.stop();
5963
super.stop(context);
6064
}
6165

0 commit comments

Comments
 (0)