From 64f8d69789c70476412a0957c73abf477e19cf9c Mon Sep 17 00:00:00 2001 From: Gesa Hentschke Date: Tue, 6 Feb 2024 07:27:22 +0100 Subject: [PATCH] [#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. --- bundles/org.eclipse.cdt.lsp.clangd/plugin.xml | 11 ++ .../clangd/ClangdConfigFileChecker.java | 172 ++++++++++++++++++ .../clangd/ClangdConfigFileMonitor.java | 88 +++++++++ .../cdt/lsp/internal/clangd/FileUtils.java | 98 ++++++++++ .../internal/clangd/editor/ClangdPlugin.java | 4 + 5 files changed, 373 insertions(+) create mode 100644 bundles/org.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/ClangdConfigFileChecker.java create mode 100644 bundles/org.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/ClangdConfigFileMonitor.java create mode 100644 bundles/org.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/FileUtils.java diff --git a/bundles/org.eclipse.cdt.lsp.clangd/plugin.xml b/bundles/org.eclipse.cdt.lsp.clangd/plugin.xml index 85e8603c..9da7b5ad 100644 --- a/bundles/org.eclipse.cdt.lsp.clangd/plugin.xml +++ b/bundles/org.eclipse.cdt.lsp.clangd/plugin.xml @@ -110,5 +110,16 @@ + + + + + + diff --git a/bundles/org.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/ClangdConfigFileChecker.java b/bundles/org.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/ClangdConfigFileChecker.java new file mode 100644 index 00000000..8572756b --- /dev/null +++ b/bundles/org.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/ClangdConfigFileChecker.java @@ -0,0 +1,172 @@ +/******************************************************************************* + * Copyright (c) 2024 Bachmann electronic 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: + * Gesa Hentschke (Bachmann electronic GmbH) - initial implementation + *******************************************************************************/ + +package org.eclipse.cdt.lsp.internal.clangd; + +import java.io.IOException; +import java.util.Optional; +import java.util.regex.Pattern; + +import org.eclipse.cdt.lsp.internal.clangd.editor.ClangdPlugin; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.Platform; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.yaml.snakeyaml.Yaml; + +/** + * Checks the .clangd file for syntax errors and notifies the user via error markers in the file and Problems view. + */ +public class ClangdConfigFileChecker { + public static final String CLANGD_MARKER = ClangdPlugin.PLUGIN_ID + ".config.marker"; //$NON-NLS-1$ + private final Pattern pattern = Pattern.compile(".*line (\\d+), column (\\d+).*"); //$NON-NLS-1$ + private boolean temporaryLoadedFile = false; + + /** + * Checks if the .clangd file contains valid yaml syntax. Adds error marker to the file if not. + * @param configFile + */ + public void checkConfigFile(IFile configFile) { + Yaml yaml = new Yaml(); + try (var inputStream = configFile.getContents()) { + try { + removeMarkerFromClangdConfig(configFile); + //throws ScannerException and ParserException: + yaml.load(inputStream); + } catch (Exception yamlException) { + addMarkerToClangdConfig(configFile, yamlException); + } + } catch (IOException | CoreException e) { + Platform.getLog(getClass()).error(e.getMessage(), e); + } + } + + private void addMarkerToClangdConfig(IFile configFile, Exception e) { + try { + var configMarker = parseYamlException(e, configFile); + var marker = configFile.createMarker(CLANGD_MARKER); + marker.setAttribute(IMarker.MESSAGE, configMarker.message); + marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_ERROR); + marker.setAttribute(IMarker.LINE_NUMBER, configMarker.line); + marker.setAttribute(IMarker.CHAR_START, configMarker.charStart); + marker.setAttribute(IMarker.CHAR_END, configMarker.charEnd); + } catch (CoreException core) { + Platform.getLog(getClass()).log(core.getStatus()); + } + } + + private class ClangdConfigMarker { + public String message; + public int line = 1; + public int charStart = -1; + public int charEnd = -1; + } + + /** + * Fetch line and char position information from exception to create a marker for the .clangd file. + * @param e + * @param file + * @return + */ + private ClangdConfigMarker parseYamlException(Exception e, IFile file) { + var marker = new ClangdConfigMarker(); + marker.message = getErrorMessage(e); + var doc = getDocument(file); + if (doc == null) { + return marker; + } + int startLine = -1; + int endLine = -1; + for (var line : toLines(e.getMessage())) { + var matcher = pattern.matcher(line); + if (matcher.matches()) { + var lineInt = Integer.parseInt(matcher.replaceAll("$1")); //$NON-NLS-1$ + var column = Integer.parseInt(matcher.replaceAll("$2")); //$NON-NLS-1$ + if (startLine == -1) { + startLine = lineInt; + } else if (endLine == -1) { + endLine = lineInt; + } + try { + if (marker.charStart == -1 && startLine > -1) { + var lineOffset = doc.getLineOffset(startLine - 1); + marker.charStart = lineOffset + column - 1; + } else if (marker.charEnd == -1 && endLine > -1) { + var lineOffset = doc.getLineOffset(endLine - 1); + marker.charEnd = lineOffset + column - 1; + } + } catch (BadLocationException bl) { + Platform.getLog(getClass()).error(bl.getMessage(), bl); + } + if (startLine > -1 && endLine > -1) + break; + } + } + //check if endChar has been found: + if (marker.charEnd == -1) { + if (marker.charStart < doc.getLength() - 1) { + marker.charEnd = marker.charStart + 1; + } else if (marker.charStart == doc.getLength() - 1 && marker.charStart > 0) { + marker.charEnd = marker.charStart; + marker.charStart--; + } else { + marker.charStart = 0; + marker.charEnd = 1; + } + } + cleanUp(file); + if (startLine > -1) { + marker.line = startLine; + } + return marker; + } + + private String[] toLines(String message) { + return Optional.ofNullable(message).map(m -> m.lines().toArray(String[]::new)).orElse(new String[] {}); + } + + private String getErrorMessage(Exception e) { + return Optional.ofNullable(e.getLocalizedMessage()) + .map(m -> m.replaceAll("[" + System.lineSeparator() + "]", " ")) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + .orElse("Unknown yaml error"); //$NON-NLS-1$ + } + + private void removeMarkerFromClangdConfig(IFile configFile) { + try { + configFile.deleteMarkers(CLANGD_MARKER, false, IResource.DEPTH_INFINITE); + } catch (CoreException e) { + Platform.getLog(getClass()).log(e.getStatus()); + } + } + + private IDocument getDocument(IFile file) { + IDocument document = FileUtils.getDocumentFromBuffer(file); + if (document != null) + return document; + document = FileUtils.loadFileTemporary(file); + if (document != null) + temporaryLoadedFile = true; + return document; + } + + private void cleanUp(IFile file) { + if (temporaryLoadedFile) { + FileUtils.disconnectTemporaryLoadedFile(file); + temporaryLoadedFile = false; + } + } + +} diff --git a/bundles/org.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/ClangdConfigFileMonitor.java b/bundles/org.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/ClangdConfigFileMonitor.java new file mode 100644 index 00000000..034de754 --- /dev/null +++ b/bundles/org.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/ClangdConfigFileMonitor.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * Copyright (c) 2024 Bachmann electronic 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: + * Gesa Hentschke (Bachmann electronic GmbH) - initial implementation + *******************************************************************************/ + +package org.eclipse.cdt.lsp.internal.clangd; + +import java.util.concurrent.ConcurrentLinkedQueue; + +import org.eclipse.cdt.lsp.internal.clangd.editor.ClangdPlugin; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IResourceChangeEvent; +import org.eclipse.core.resources.IResourceChangeListener; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.WorkspaceJob; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.ui.statushandlers.StatusManager; + +/** + * Monitor changes in .clangd files in the workspace and triggers a yaml checker + * to add error markers to the .clangd file when the edits causes yaml loader failures. + */ +public class ClangdConfigFileMonitor { + private static final String CLANGD_CONFIG_FILE = ".clangd"; //$NON-NLS-1$ + private final ConcurrentLinkedQueue pendingFiles = new ConcurrentLinkedQueue<>(); + private final IWorkspace workspace; + private final ClangdConfigFileChecker checker = new ClangdConfigFileChecker(); + + private final IResourceChangeListener listener = new IResourceChangeListener() { + @Override + public void resourceChanged(IResourceChangeEvent event) { + if (event.getDelta() != null && event.getType() == IResourceChangeEvent.POST_CHANGE) { + try { + event.getDelta().accept(delta -> { + if ((delta.getKind() == IResourceDelta.ADDED || delta.getKind() == IResourceDelta.REMOVED + || (delta.getFlags() & IResourceDelta.CONTENT) != 0) + && CLANGD_CONFIG_FILE.equals(delta.getResource().getName())) { + if (delta.getResource() instanceof IFile file) { + pendingFiles.add(file); + checkJob.schedule(100); + } + } + return true; + }); + } catch (CoreException e) { + StatusManager.getManager().handle(e, ClangdPlugin.PLUGIN_ID); + } + } + } + }; + + public ClangdConfigFileMonitor(IWorkspace workspace) { + this.workspace = workspace; + } + + private final WorkspaceJob checkJob = new WorkspaceJob("Check .clangd file") { //$NON-NLS-1$ + + @Override + public IStatus runInWorkspace(IProgressMonitor monitor) throws CoreException { + while (pendingFiles.peek() != null) { + checker.checkConfigFile(pendingFiles.poll()); + } + return Status.OK_STATUS; + } + + }; + + public ClangdConfigFileMonitor start() { + workspace.addResourceChangeListener(listener); + return this; + } + + public void stop() { + workspace.removeResourceChangeListener(listener); + } +} diff --git a/bundles/org.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/FileUtils.java b/bundles/org.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/FileUtils.java new file mode 100644 index 00000000..249ba14c --- /dev/null +++ b/bundles/org.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/FileUtils.java @@ -0,0 +1,98 @@ +/******************************************************************************* + * Copyright (c) 2024 Bachmann electronic 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: + * Gesa Hentschke (Bachmann electronic GmbH) - initial implementation + *******************************************************************************/ + +package org.eclipse.cdt.lsp.internal.clangd; + +import java.util.Optional; + +import org.eclipse.core.filebuffers.FileBuffers; +import org.eclipse.core.filebuffers.ITextFileBuffer; +import org.eclipse.core.filebuffers.ITextFileBufferManager; +import org.eclipse.core.filebuffers.LocationKind; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Platform; +import org.eclipse.jface.text.IDocument; + +public class FileUtils { + + private FileUtils() { + // do not instantiate + } + + /** + * Loads a files document for temporary usage. {@link FileUtils#disconnectTemporaryLoadedFile(IFile)} has to be called on the file after usage! + * @param file + * @return temporary loaded document for the given file or null. + */ + public static IDocument loadFileTemporary(IFile file) { + if (file == null) { + return null; + } + IDocument document = null; + + if (file.getType() == IResource.FILE) { + var bufferManager = getBufferManager(); + if (bufferManager == null) + return document; + try { + bufferManager.connect(file.getFullPath(), LocationKind.IFILE, new NullProgressMonitor()); + } catch (CoreException e) { + Platform.getLog(FileUtils.class).error(e.getMessage(), e); + return document; + } + + ITextFileBuffer buffer = bufferManager.getTextFileBuffer(file.getFullPath(), LocationKind.IFILE); + if (buffer != null) { + document = buffer.getDocument(); + } + } + + return document; + } + + /** + * When a files document has been obtained via {@link FileUtils#loadFileTemporary(IFile)}, then the file has to be disconnected from it's buffer manager. + * @param file + */ + public static void disconnectTemporaryLoadedFile(IFile file) { + Optional.ofNullable(getBufferManager()).ifPresent(bm -> { + try { + bm.disconnect(file.getFullPath(), LocationKind.IFILE, new NullProgressMonitor()); + } catch (CoreException e) { + Platform.getLog(FileUtils.class).error(e.getMessage(), e); + } + }); + } + + /** + * Tries to fetch the document for the given file. Returns the document when the file is already in the text file buffer or null if not. + * @param file + * @return document for the given file or null + */ + public static IDocument getDocumentFromBuffer(IFile file) { + if (file == null) { + return null; + } + return Optional.ofNullable(getBufferManager()) + .map(bm -> bm.getTextFileBuffer(file.getFullPath(), LocationKind.IFILE)).map(b -> b.getDocument()) + .orElse(null); + } + + private static ITextFileBufferManager getBufferManager() { + return FileBuffers.getTextFileBufferManager(); + } + +} diff --git a/bundles/org.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/editor/ClangdPlugin.java b/bundles/org.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/editor/ClangdPlugin.java index 2957097c..8bc693c6 100644 --- a/bundles/org.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/editor/ClangdPlugin.java +++ b/bundles/org.eclipse.cdt.lsp.clangd/src/org/eclipse/cdt/lsp/internal/clangd/editor/ClangdPlugin.java @@ -15,6 +15,7 @@ package org.eclipse.cdt.lsp.internal.clangd.editor; import org.eclipse.cdt.lsp.internal.clangd.CProjectChangeMonitor; +import org.eclipse.cdt.lsp.internal.clangd.ClangdConfigFileMonitor; import org.eclipse.core.resources.IWorkspace; import org.eclipse.ui.plugin.AbstractUIPlugin; import org.osgi.framework.BundleContext; @@ -27,6 +28,7 @@ public class ClangdPlugin extends AbstractUIPlugin { private IWorkspace workspace; private CompileCommandsMonitor compileCommandsMonitor; private CProjectChangeMonitor cProjectChangeMonitor; + private ClangdConfigFileMonitor configFileMonitor; // The plug-in ID public static final String PLUGIN_ID = "org.eclipse.cdt.lsp.clangd"; //$NON-NLS-1$ @@ -49,6 +51,7 @@ public void start(BundleContext context) throws Exception { workspace = workspaceTracker.getService(); compileCommandsMonitor = new CompileCommandsMonitor(workspace).start(); cProjectChangeMonitor = new CProjectChangeMonitor().start(); + configFileMonitor = new ClangdConfigFileMonitor(workspace).start(); } @Override @@ -56,6 +59,7 @@ public void stop(BundleContext context) throws Exception { plugin = null; compileCommandsMonitor.stop(); cProjectChangeMonitor.stop(); + configFileMonitor.stop(); super.stop(context); }